# Events

The [API reference](/api) holds every field and an example of each event; this page is the
working overview.

## Events Hijinks sends

`POST` to your bridge URL with `Content-Type: application/json` and three headers:
`Hijinks-Signature`, `Hijinks-Event-Id` and `Hijinks-Workspace`. Answer any `2xx` within 5 seconds.

| type | When | Fields | Retries |
| --- | --- | --- | --- |
| `message.create` | Post a message in a channel | `channel`, `message` | +1 s, +4 s, then dropped |
| `message.update` | Replace a message Hijinks created (each question, answer counts, reveal, final board) | `channel`, `message` (same `message.id`) | a question, reveal or final board: +1 s, +4 s, then dropped; anything else never — the next update supersedes it |
| `message.ephemeral` | A reply only the member who acted should see | `channel`, `user`, `message` | +1 s, +4 s, then dropped |
| `dm.create` | A private message to a member (async rounds, Know Your Team, reminders) | `user`, `message` | +1 s, +4 s, then dropped |
| `ping` | "Send test event" in the dashboard | — | never |

Every event also carries `id`, `type`, `workspace`, `sequence`, `createdAt` and `expiresAt`
(when it goes stale; see [Retries and ordering](#retries-and-ordering)). A question looks like
this: an update of the message the game started with.

```json
{
  "id": "evt_01JA2Z8KQ4M6V3XG5H7N9PRSTW",
  "type": "message.update",
  "workspace": "6f0c2b9e-8d4a-4f6b-9c1e-2a7d5e0b4c83",
  "sequence": 1760018640000001,
  "createdAt": "2026-10-09T14:04:00.000Z",
  "expiresAt": "2026-10-09T14:14:00.000Z",
  "channel": "town-square",
  "message": {
    "id": "msg_01JA2Z8KQ4M6V3XG5H7N9PRSTW",
    "kind": "question",
    "text": "*🧠 Question 3 of 5 · Film · Medium*\n**Which film was marketed with …?**\n\n*⏱ 30 seconds to answer*",
    "buttons": [
      [{ "label": "Alien", "action": "answer_2_0" }],
      [{ "label": "Gravity", "action": "answer_2_1" }]
    ],
    "image": null,
    "awaitsReply": false,
    "data": { "questionIndex": 2, "totalQuestions": 5, "category": "Film" }
  }
}
```

## Rendering a message

- **text** is chat-flavoured Markdown, ready to print: `*italic*`, `**bold**`, code, links,
  quotes. **A bare `\n` is a line break**, as in most chat tools. If your chat renders strict
  CommonMark, turn on hard line breaks (markdown-it's `breaks: true`, for example), or lines will
  run together.
- **buttons** are rows, top to bottom. A button is either `{ label, action }` — send `action` back
  when it is pressed — or `{ label, url }`, a link to open (the Pictionary drawing room, for one).
- **image** is a picture or GIF URL to show with the message, or null.
- **awaitsReply** means Hijinks would like the member's next typed message to answer this one;
  offer a reply box if your chat has one. In a DM no reply is needed: the member's next message
  there answers it.
- **kind** names the message (`question`, `time_up`, `game_end`, `leaderboard`, `wordle_board`
  and 27 more) so a bridge can special-case a few and print the rest. **data** carries the view
  model behind it, for bridges that draw their own UI.
- A `message.update` for a message first sent as a DM arrives with `channel: "dm:<user id>"`. Send
  button presses and typed messages from a member's DM back the same way, as `action` and
  `message` events with `channel: "dm:<their own user.id>"`: replies then go to the DM, and a typed
  message there answers the Know Your Team question (or any other prompt) waiting in it. Any other
  channel id starting with `dm:` is refused.

## Events your bridge sends

`POST https://api.hijinks.team/hooks/<workspace id>`, signed the same way. `user` is
`{ "id", "name" }` (ids up to 128 characters; send the name whenever you have it, it is what
leaderboards print). `channel` is an id, or `{ "id", "name" }`. Unknown fields are refused, so a
typo fails loudly. Every event may also carry your own `id` for the request (up to 128
characters, unique per request): send it, and retries become safe.

| type | Meaning | Fields |
| --- | --- | --- |
| `command` | A member ran `/hijinks <text>` | `channel`, `user`, `text` |
| `action` | A member pressed a button | `channel`, `user`, `messageId`, `action` |
| `message` | A member typed something (word games, rebus, charades, replies) | `channel`, `user`, `text`, `replyTo?` |

A typed `message` that answers nothing in play is accepted and dropped, so a bridge can forward
every message in a channel without filtering. All visible output arrives as outbound events, never
in the HTTP response.

| Status | Meaning |
| --- | --- |
| `202` | Accepted: `{"ok":true}`. What it causes arrives as outbound events. |
| `400` | Malformed JSON, or the body does not match the schema; `field` names the culprit. |
| `401` | Missing or wrong `Hijinks-Signature`. |
| `404` | Unknown, non-webhook or disconnected workspace. |
| `409` | Signature valid, but the timestamp is more than 5 minutes off (replay or clock drift). |
| `413` | Body larger than 64 KB. |
| `429` | More than 500 requests a second from your IP address (checked first, signed or not), or more than 250 signed requests a second for this workspace. Wait `Retry-After` seconds and send it again. |

### Rate limits and retries

Each button press and typed answer is one request, so the limit is set for a whole game answering
in the same second: **250 signed requests a second per workspace** covers 200 players, the largest
per-game cap of any plan that has one, with room for polls. Bridges for several workspaces behind
one egress address share that address's 500 a second.

Hijinks acts on a request once. A request repeating an `id` sent in the last 24 hours is answered
`202` and ignored; without an `id`, only an exact replay (same signature, same bytes) is. So:

- **Retry** a timeout, a dropped connection, a `5xx` or a `429` (after `Retry-After`), with the
  same body and the same `id`, re-signed with the current time.
- **Don't retry** any other `4xx`: the request itself is wrong, and `field` says where.

Without an `id`, a retry re-signed with a new timestamp counts as a new request: a word-game guess
sent twice costs two turns.

## Retries and ordering

- Every delivery has a 5-second deadline. Creates, DMs, ephemeral replies and the updates that
  start a phase of a game (a question, a reveal, the final board) are retried after 1 and 4
  seconds, then dropped — a question that arrives 20 seconds late is worse than none. Other updates
  (answer counts, tallies) and pings are never retried: the next update supersedes them.
- A retry carries the same event id. **Dedupe on `Hijinks-Event-Id`**, and answer `2xx` before
  doing slow work.
- `sequence` increases per workspace, so events that arrive out of order can be put back in order:
  skip an update older than one you have applied.
- `expiresAt` says when an event stops being worth showing: 60 seconds for ephemeral replies and
  pings, 10 minutes for a live game's messages and command answers, `null` (never) for self-paced
  rounds, word games, icebreakers and digests. Drop an event past it, unapplied.
- The game runs on its own clock and never stalls a round for a bridge that is down. A **push**
  bridge that is down misses what is sent meanwhile. A **polling** bridge finds it all waiting
  for 24 hours, and drops what expired on the way ([Catching up](/polling#catching-up-after-downtime)).
- Your bridge URL must be public HTTPS. Hijinks does not follow redirects and will not deliver to
  private or loopback addresses.
