# Polling instead of an inbound URL

By default Hijinks **pushes** each event to your bridge's public HTTPS address. If your chat
server lives on an intranet with outbound access only, the bridge can **poll** instead: it asks
Hijinks for new events every couple of seconds. Nothing on your network has to accept a
connection from the internet.

|  | Push (default) | Poll |
| --- | --- | --- |
| Your bridge needs | A public HTTPS address, a certificate, a proxy rule | Outbound HTTPS to `api.hijinks.team` |
| Events arrive | At once | Within one poll interval (2 seconds in the reference bridge) |
| If the bridge is down | Each event is retried for 5 seconds, then dropped | Events wait up to 24 hours; stale ones are dropped on catch-up |
| Connect in the dashboard with | Your bridge's URL as the target URL | No target URL |

Both modes carry the same events: the same envelopes, ids and sequence numbers, the same
signature. Everything on [What works over a bridge](/bridged-chats) works in either.

## Set it up

1. In the dashboard, connect the custom platform and choose **Poll** under "How events reach
   your bridge". The endpoint is then a polling one: Hijinks holds its events instead of sending
   them, and the dashboard shows the events URL next to the inbound URL. Setting a target URL
   later in the workspace settings switches it to push; clearing it switches back. **Before you
   switch to push, let the bridge drain what is held with one last poll**: from that moment polls
   are refused, and events not fetched by then are never pushed.
2. Start the reference bridge with `--poll` (or `BRIDGE_MODE=poll`). `BRIDGE_URL` is the address
   Mattermost uses to reach the bridge, which may be internal. Keep its state file on a volume
   (the Docker image's `/data`): the cursor lives there. The
   <a href="/webhooks/mattermost/README.md">README</a> has an intranet section.
3. Press **Send test event**. In poll mode it queues a `ping` and reports success when the bridge
   has polled within the last minute.

## Firewall and proxy

The bridge only makes outbound requests: HTTPS (TCP 443) to `api.hijinks.team`. Ask for that one
destination. If outbound traffic must go through a corporate proxy, set `HTTPS_PROXY` (and
`NO_PROXY` for your chat server's address) and, on Node 24 — the reference bridge's Docker image —
`NODE_USE_ENV_PROXY=1`, which makes Node's built-in `fetch` use them.

## The request

```http
GET /hooks/<workspace id>/events?after=4182 HTTP/1.1
Host: api.hijinks.team
Hijinks-Signature: t=1760018640,v1=<hex HMAC-SHA256(secret, "1760018640.after=4182")>
```

- **Sign the query string** exactly as sent (everything after `?`) in place of a body. With no
  query string, sign the empty string: `"<t>."`.
- **`after`**: the `next` cursor from your previous poll. Store it with the rest of the bridge's
  state, so a restart carries on where it stopped.
- **`after=latest`** answers no events and the newest cursor: start from now. Use it on a first
  start, and whenever the saved cursor is lost. Leaving `after` out instead starts from the
  oldest event held, which replays up to 24 hours of games into your channels.
- **`limit`**: 1 to 100, default 100.

A cursor never skips an event: an event still being written when you poll is served after the
cursor you got, never before it.

## The response

```json
{
  "events": [
    { "id": "evt_01JA2ZD9E0F1G2H3J4K5M6N7P8", "type": "ping", "workspace": "6f0c2b9e-…", "sequence": 1760018880000007, "createdAt": "2026-10-09T14:08:00.000Z" }
  ],
  "next": "4183",
  "hasMore": false,
  "gap": false
}
```

- **Verify `Hijinks-Signature` on the response** over its exact bytes, exactly as you would a
  pushed event ([Verify signatures](/signatures)), before acting on anything in it.
- Handle each event as if it had been pushed, oldest first. Dedupe on `id`: if your bridge stops
  between handling an event and saving the cursor, the next poll repeats it.
- When `hasMore` is true, poll again at once. Otherwise wait for your interval.
- Events are kept for **24 hours**. When your `after` is older than that, the page says
  `"gap": true`: something expired before you fetched it. Post a note in the channel if you like
  ("Hijinks was unreachable; some messages were missed") and carry on from `next`.

## Catching up after downtime

Events wait while your bridge is down, but most of them are worth nothing an hour later. Every
event carries `expiresAt`:

- **Drop an event past its `expiresAt`**, unapplied. Replies to one member and pings expire after
  60 seconds; a live game's messages and the answers to commands after 10 minutes; self-paced
  rounds, word games with their own end, icebreakers and digests never (`null`).
- **Collapse updates:** of several `message.update`s for one `message.id` in a page, apply only
  the one with the highest `sequence`.
- An update for a message whose create you dropped is dropped with it, as usual.

The reference bridge does all three.

## When your chat refuses a message

- **A create** (`message.create`, `dm.create`) your chat refused: don't advance past it. Poll the
  same `after` again and retry it until it goes through or expires; the events you already
  handled come back too, and you skip them by `id`.
- **An update or an ephemeral reply**: drop it. The next update supersedes it, and a reply is
  stale within a minute anyway.

## If the bridge stops polling

The dashboard calls a polling bridge **failing** when events have waited for it for 5 minutes
without a poll, and the workspace's owner gets one email per outage. The next poll clears it.

## Errors

The checks and their order are those of `POST /hooks/{workspaceId}`: `429` for too many
requests (polls count against the same limits), `404` for an unknown workspace, `401` for a bad
signature over the query string, `409` for a timestamp more than 5 minutes off. Then `400` names
a malformed, unknown or repeated query parameter, and `403` means the endpoint has a target URL,
so its events are pushed and never held. The [API reference](/api) has the full contract.
