# Receive events

> What Amedeus sends your app, how to verify it, and how retries work.

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

1. **Verify the signature** of every request (below).
2. **Answer `2xx` within 3 seconds.** Do the work afterwards, for example from a queue. Anything else is
a failed delivery.
3. **Be idempotent.** An event can arrive more than once (retries). `event_id` is the same each time.

## 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](/guides/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

```json
{
  "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](/api-reference/) 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

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.

**JavaScript**

```js

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))
  );
}
```

**Python**

```python

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

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

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.

