> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quivr.thevibecompany.co/llms.txt
> Use this file to discover all available pages before exploring further.

# X lists in real time: webhook mode

> Receive the posts of an X list in real time through webhooks

With webhook mode on, an `x_list` Connector Instance receives the posts of its
list's members within seconds, through an X Filtered Stream linked to a
webhook. Polling stays on as the fallback. Read the [X list guide](/connectors/x) first:
everything there still applies.

<h2 id="what-you-bring">
  What you bring
</h2>

* **A public URL for the API.** Set `public_url` in the `QUIVR_CONFIG` of every
  `api` and `worker` process to the address where X reaches your API, for
  example `https://quivr.example.com`. Each instance's webhook address is
  `<public_url>/v0/connector-webhooks/<connector_id>`; the instance read shows
  it as `webhook_url`. The route needs no API key, because X signs every
  request. Put it behind your usual rate limiting.
* **The x-list plugin reachable from the API.** The API relays each delivery
  to the plugin, so pin it on the `api` processes too (the Railway image
  runs it beside the API).
* **An app with Filtered Stream and webhooks.** Your X plan must include the
  Filtered Stream and webhook endpoints. The bearer token must manage the
  app's stream rules and webhooks.
* **The consumer secret**, deposited with the bearer token
  (`consumer_secret`). The plugin answers X's CRC check and verifies every
  delivery's `x-twitter-webhooks-signature` with it.

<h2 id="turn-it-on">
  Turn it on
</h2>

Add a `webhook` block to the instance config:

| Field | Default | Meaning |
| - | - | - |
| `enabled` | `false` | Receive posts through webhooks |
| `resync_interval_seconds` | 900 | How often members, rules and the webhook are checked |
| `poll_interval_seconds` | 900 | While webhooks work, polling runs at most this often |
| `max_rules` | 1000 | Most stream rules this list may use |
| `max_rule_length` | 512 | Longest rule, in characters |

Set `max_rules` and `max_rule_length` to your plan's limits. Rules are shared
by every instance of the same X app, so leave room for the others.
Deletion rechecks and resyncs run during polls, so while webhooks work they
also wait up to `poll_interval_seconds`: keep it within your recheck needs.

<h2 id="what-the-plugin-sets-up">
  What the plugin sets up
</h2>

During pull runs, the plugin reads the list members and turns them into rules
`from:<user id> OR …`, each within `max_rule_length`, tagged
`quivr:<connector_id>`. It adds and deletes only what changed, so a resync
with the same members writes nothing. It registers the webhook address (X
checks it with a CRC request) and links it to the stream. A list that needs
more than `max_rules` rules stays on polling (`rule_limit_exceeded`).

Deliveries go through the same mapping as polling. A post seen both ways is
one Record with one Version.

<h2 id="health-and-fallback">
  Health and fallback
</h2>

`health.push` shows the webhook side:

| `state` | Meaning |
| - | - |
| `pending` | Not set up yet; `webhook_url_missing` means `public_url` is not set |
| `active` | Deliveries arrive; polling runs every `poll_interval_seconds` |
| `degraded` | See `error`; polling is back at the instance's interval |

| `error.code` | Cause | Recovery |
| - | - | - |
| `webhook_invalid` (access) | X invalidated the webhook, for example after failed CRC checks | The next resync asks X to check it again |
| `webhook_refused`, `stream_forbidden` (access) | X refused the webhook or the rules | Fix the app's access; the next resync retries |
| `plugin_unavailable` | A delivery found the plugin down; X got a 503 and retries | The next accepted delivery |
| `missed_deliveries` | Polling found posts no delivery brought | The next delivery with posts |
| `consumer_secret_missing` (access) | No consumer secret deposited | Deposit it |

An access error shows as the `access_error` Connector Health state while
polling keeps collecting. To avoid false alarms, polling holds back posts
younger than one minute while webhooks work. A member added to the list shows
up at the next resync.

<h2 id="check-it-with-a-real-account">
  Check it with a real account
</h2>

CI tests webhook mode against a local fake X only. To check a real account:
turn it on for a test list, wait for `health.push.state` `active`, post from a
member, and watch the Record arrive within seconds. Then compare
`health.usage` with the X console.

<h2 id="turning-it-off">
  Turning it off
</h2>

Set `enabled` to `false`, or disable the instance. The rules tagged
`quivr:<connector_id>` and the webhook stay at X until you delete them in the
developer console or through the X API.
