Receive events
Amedeus sends your app an HTTPS POST when something happens that it asked to hear about. You choose
the events and the request URL under Manage → Installed apps → the app → Event subscriptions.
What you must do
Section titled “What you must do”- Verify the signature of every request (below).
- Answer
2xxwithin 3 seconds. Do the work afterwards, for example from a queue. Anything else is a failed delivery. - Be idempotent. An event can arrive more than once (retries).
event_idis the same each time.
Event types
Section titled “Event types”| Event | When | Needs |
|---|---|---|
message.channel |
A person posts in a channel the app belongs to | channels:history |
message.im |
A person sends the app a direct message | channels:history |
app_mention |
A person mentions the app, in a channel or a direct message | app_mentions:read |
Messages written by apps or AI agents never reach apps, so two apps can’t trigger each other.
One message can arrive as more than one event. A message that mentions your app in a channel is
delivered as message.channel and as app_mention; a direct message that mentions it, as message.im
and app_mention. A retry also repeats an event. Subscribe only to what you need, and answer each
event.message_id once, as the templates do.
A direct message with an app exists only while the app accepts messages: App Home → Allow people to
message this app (off by default). Receiving message.im and accepting messages are separate
switches.
The payload
Section titled “The payload”{ "type": "event_callback", "event_id": "f3e62f45-273b-489e-9aed-c2dca1437849", "event_time": 1794511135, "workspace_id": "80f1d0c5-9e89-4657-88e0-754997a08bfa", "app_id": "2362dcb0-4f20-4fc7-b1a6-9cd06923679d", "bot_user_id": "e83f53c0-f56b-42a5-89e4-f18b5c9a31f4", "event": { "type": "message.im", "message_id": "e2a4be8f-eaa6-4e21-b1da-8b022c30441d", "channel": "890d8292-33b6-4d9a-9f05-772c8571137e", "channel_kind": "dm", "user": "7118b1bc-22c0-4bdd-98bd-2014b89b0e66", "text": "probando el sobre del evento", "parent_id": null, "mentions": [], "created_at": "2026-11-12T19:18:55.014928+00:00" }}| Field | Meaning |
|---|---|
type |
event_callback, or url_verification while you save the URL |
event_id |
Unique per event. The same on every retry: use it to ignore duplicates |
event_time |
When it was sent, in seconds since the epoch |
workspace_id |
The workspace where it happened |
app_id |
Your app |
bot_user_id |
Your app’s own user. Compare it with mentions to know whether you were named |
event.type |
One of the event types above |
event.message_id |
The message |
event.channel |
The conversation |
event.channel_kind |
public, private or dm |
event.user |
Who wrote it |
event.text |
Markdown, as written |
event.parent_id |
The thread the message replies to, or null |
event.mentions |
Ids of the people mentioned |
event.created_at |
When the message was written |
To answer, call the Web API with your token, for example POST /messages with
channel set to event.channel and parent_id set to reply in a thread.
Verifying a request
Section titled “Verifying a request”Every request carries these headers:
| Header | Value |
|---|---|
X-Amedeus-Signature |
v0= + hex HMAC-SHA256 of v0:{timestamp}:{raw body}, keyed with the signing secret |
X-Amedeus-Request-Timestamp |
Seconds since the epoch |
X-Amedeus-Retry-Num |
0 the first time, then 1, 2, 3 |
X-Amedeus-Retry-Reason |
On retries only: why the previous attempt failed (see below) |
X-Amedeus-Event-Type |
The event type, or slash_command, or url_verification |
The signing secret is under Event subscriptions → Signing secret. Compute the signature over the raw body (before parsing), compare in constant time, and reject timestamps older than 5 minutes.
import crypto from "node:crypto";
function verify(req, rawBody, signingSecret) { const timestamp = req.headers["x-amedeus-request-timestamp"]; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = "v0=" + crypto.createHmac("sha256", signingSecret).update(`v0:${timestamp}:${rawBody}`).digest("hex"); const given = String(req.headers["x-amedeus-signature"] ?? ""); return ( given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)) );}import hashlibimport hmacimport time
def verify(headers, raw_body: bytes, signing_secret: bytes) -> bool: timestamp = headers.get("X-Amedeus-Request-Timestamp", "") if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300: return False signed = f"v0:{timestamp}:".encode() + raw_body expected = "v0=" + hmac.new(signing_secret, signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, headers.get("X-Amedeus-Signature", ""))Verifying the URL
Section titled “Verifying the URL”When you save a URL, Amedeus first sends {"type":"url_verification","challenge":"…"}, signed like any
other request. Answer 200 with the challenge as plain text, or as {"challenge":"…"}. The URL is not
saved until it does. The page then shows when it last answered (“Verified”).
A slash command with its own URL is verified the same way when you save it, so it must answer the challenge too. A command without one uses the events URL, which was already verified.
Retries
Section titled “Retries”A failed delivery is tried again after 1 minute, 5 minutes and 30 minutes: four attempts in
all. Redirects are not followed, so give the final address (with https and any trailing slash).
X-Amedeus-Retry-Reason says why the last attempt failed:
| Reason | Meaning |
|---|---|
http_error |
Your URL answered a status other than 2xx |
http_timeout |
No answer within 3 seconds |
connection_failed |
The server couldn’t be reached |
ssl_error |
The certificate couldn’t be verified |
unknown_error |
Anything else |
If almost every delivery fails (more than 95% of at least 20 in an hour), Amedeus switches the app’s events off and records it in the activity log. Saving the settings again turns them back on. Enable events pauses them on purpose, keeping the URL and your choices; slash commands are not affected.