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

# Webhooks overview

> Real-time event delivery to your HTTPS endpoints with HMAC signing, retry policies, and event filtering

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Webhooks deliver events from account or keyword monitors to your server in real time. Every delivery is signed with HMAC-SHA256 so you can verify authenticity.

Use webhooks when `tweet.new`, `tweet.reply`, `tweet.quote`, `tweet.retweet`,
`tweet.media`, `tweet.link`, `tweet.poll`, `tweet.mention`, `tweet.hashtag`,
`tweet.longform`, `profile.avatar.changed`, `profile.banner.changed`,
`profile.name.changed`, `profile.username.changed`, `profile.bio.changed`,
`profile.location.changed`, `profile.url.changed`, `profile.verified.changed`,
`profile.protected.changed`, `profile.pinned_tweet.changed`, or
`profile.unavailable.changed` events from tracked accounts must reach your app
without polling. Keyword monitors support tweet event types only. The setup
returns a monitor ID, webhook ID, one-time signing secret, and signed JSON
deliveries. Webhook operations are free. Active monitors cost 21 credits/hour
and include stored events plus webhook delivery.

<CardGroup cols={3}>
  <Card title="Account or keyword monitor" icon="radio">
    Output: monitor ID, username or query, and selected event types.
    Cost: 21 credits/hour while active.
  </Card>

  <Card title="Webhook endpoint" icon="webhook">
    Output: webhook ID, URL, event types, and one-time `secret`.
    Cost: free.
  </Card>

  <Card title="Signed delivery" icon="shield-check">
    Output: HTTPS POST with JSON body and HMAC headers.
    Cost: included with the active monitor.
  </Card>
</CardGroup>

## Choose the webhook source

<CardGroup cols={2}>
  <Card title="Account activity" icon="user-round">
    Create `POST /monitors` when one X account should emit selected tweet and
    profile event types. Store `monitorId`, `username`, `xUserId`, and
    `eventTypes`.
  </Card>

  <Card title="Keyword matches" icon="search">
    Create `POST /monitors/keywords` when a query should emit matching tweet
    events. Store `keywordMonitorId`, `query`, and `eventTypes`.
  </Card>

  <Card title="Receiver endpoint" icon="webhook">
    Create `POST /webhooks` after the monitor. Store the webhook `id`, URL,
    selected `eventTypes`, and one-time `secret` before sending tests.
  </Card>

  <Card title="Replay and audit" icon="rotate-ccw">
    Use `GET /events` for stored monitor events and
    `GET /webhooks/{id}/deliveries` for delivery attempts. Join on
    `streamEventId`.
  </Card>
</CardGroup>

## Quick setup

Get webhooks working in 3 steps:

<Steps>
  <Step title="Create a monitor">
    Choose an account monitor for one X account, or a keyword monitor for matching query results.

    <CodeGroup>
      ```bash Account monitor theme={null}
      curl -X POST https://xquik.com/api/v1/monitors \
        -H "x-api-key: xq_YOUR_KEY_HERE" \
        -H "Content-Type: application/json" \
        -d '{
          "username": "elonmusk",
          "eventTypes": ["tweet.new", "tweet.reply"]
        }' | jq
      ```

      ```bash Keyword monitor theme={null}
      curl -X POST https://xquik.com/api/v1/monitors/keywords \
        -H "x-api-key: xq_YOUR_KEY_HERE" \
        -H "Content-Type: application/json" \
        -d '{
          "query": "xquik launch",
          "eventTypes": ["tweet.new", "tweet.reply"]
        }' | jq
      ```
    </CodeGroup>

    Store the returned monitor `id`; account events include `username`, keyword events include `query`.
  </Step>

  <Step title="Register a webhook">
    Provide an HTTPS URL and select which event types to receive. Xquik generates a signing secret. Store it securely, it is only returned once.

    ```bash theme={null}
    curl -X POST https://xquik.com/api/v1/webhooks \
      -H "x-api-key: xq_YOUR_KEY_HERE" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://your-server.com/webhook",
        "eventTypes": ["tweet.new", "tweet.reply"]
      }' | jq
    ```

    **Response:**

    ```json theme={null}
    {
      "id": "15",
      "url": "https://your-server.com/webhook",
      "eventTypes": ["tweet.new", "tweet.reply"],
      "secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
      "createdAt": "2026-02-24T10:30:00.000Z"
    }
    ```
  </Step>

  <Step title="Verify signatures">
    When events arrive, verify the `X-Xquik-Signature` header using your webhook secret to confirm authenticity. See [Signature Verification](/webhooks/verification) for implementation details.

    Send a test payload before connecting production logic:

    ```bash theme={null}
    curl -X POST https://xquik.com/api/v1/webhooks/15/test \
      -H "x-api-key: xq_YOUR_KEY_HERE" | jq
    ```
  </Step>
</Steps>

## How it works

```text theme={null}
Account or keyword monitor event -> Xquik -> Your Webhook Endpoint
```

## Delivery format

Webhook events are delivered as HTTPS POST requests. Each monitor event is sent
as its own payload. If one active monitor check finds multiple new matching
tweets, expect multiple POST requests, one per matched tweet event, instead of
one batched payload.

### Headers

<CardGroup cols={2}>
  <Card title="Content-Type" icon="file-json">
    `application/json`. Payloads are always JSON.
  </Card>

  <Card title="User-Agent" icon="id-card">
    `xquik-webhooks/1.0 (+https://xquik.com)`. Identifies Xquik traffic.
  </Card>

  <Card title="X-Xquik-Timestamp" icon="clock">
    Unix epoch milliseconds. Used in the signing string and for replay window enforcement.
  </Card>

  <Card title="X-Xquik-Nonce" icon="fingerprint">
    16 random bytes in hex. Reject duplicates within the replay window.
  </Card>

  <Card title="X-Xquik-Signature" icon="shield-check">
    `sha256=HMAC_HEX_DIGEST`. HMAC-SHA256 of `<timestamp>.<nonce>.<rawBody>`.
  </Card>
</CardGroup>

### Payload body

```json theme={null}
{
  "eventType": "tweet.new",
  "schemaVersion": 1,
  "deliveryId": "502",
  "streamEventId": "9001",
  "occurredAt": "2026-02-24T14:22:00.000Z",
  "username": "elonmusk",
  "data": {
    "id": "1893456789012345678",
    "text": "The future is now.",
    "author": {
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk"
    },
    "isRetweet": false,
    "isReply": false,
    "isQuote": false,
    "createdAt": "2026-02-24T14:22:00.000Z"
  }
}
```

<CardGroup cols={2}>
  <Card title="eventType" icon="radio">
    Type `string`. Event type selected by the source monitor and webhook.
  </Card>

  <Card title="schemaVersion" icon="badge-check">
    Type `number`. Webhook payload schema version. Current value is `1`.
  </Card>

  <Card title="deliveryId" icon="truck">
    Type `string`. Webhook delivery attempt ID. Store it as the
    delivery-level idempotency key and use it for delivery-log correlation.
  </Card>

  <Card title="streamEventId" icon="link">
    Type `string`. Stored monitor event ID. Store it as the event-level
    de-dupe key when one monitor event should be processed once across webhook
    retries or endpoint changes.
  </Card>

  <Card title="occurredAt" icon="clock">
    Type `string`. ISO timestamp for when the event occurred.
  </Card>

  <Card title="username" icon="user">
    Type `string`. X username for account monitor events. Omitted for keyword-only monitor events and `webhook.test`.
  </Card>

  <Card title="query" icon="search">
    Type `string`. Keyword query that matched the event. Present for keyword monitor events.
  </Card>

  <Card title="data" icon="braces">
    Type `object`. Raw event object for the monitored tweet activity.
  </Card>
</CardGroup>

<Note>
  The `data` field contains the raw tweet object for the monitor event. Fields may vary by tweet type.
</Note>

### Receiver storage row

After signature verification succeeds, store a compact receiver row before handing the event to workers. Use `deliveryId` for delivery-level retries and `streamEventId` for event-level processing. Do not store endpoint signing values, the raw request body, the raw signature, or full headers in shared incident rows.

```json theme={null}
{
  "record_type": "webhook_receiver_event",
  "webhook_id": "15",
  "delivery_id": "502",
  "stream_event_id": "9001",
  "event_type": "tweet.new",
  "source": "account",
  "username": "elonmusk",
  "signature_verified": true,
  "nonce_cache_key": "webhook:15:nonce_hash",
  "occurred_at": "2026-02-24T14:22:00.000Z",
  "receiver_status": 202,
  "event_join": "GET /api/v1/events/9001",
  "delivery_log": "GET /api/v1/webhooks/15/deliveries"
}
```

## Event data shapes

Each event type includes a `data` object. Tweet events contain the raw tweet. The `webhook.test` event contains a test message and timestamp. The examples below show the most common fields.

### tweet.new

A new original tweet posted by the monitored account.

```json theme={null}
{
  "eventType": "tweet.new",
  "username": "elonmusk",
  "data": {
    "id": "1893456789012345678",
    "text": "The future is now.",
    "author": {
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk"
    },
    "isRetweet": false,
    "isReply": false,
    "isQuote": false,
    "createdAt": "2026-02-24T14:22:00.000Z"
  }
}
```

### tweet.quote

A quote tweet posted by the monitored account.

```json theme={null}
{
  "eventType": "tweet.quote",
  "username": "elonmusk",
  "data": {
    "id": "1893456789012345679",
    "text": "Interesting take on this.",
    "author": {
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk"
    },
    "isRetweet": false,
    "isReply": false,
    "isQuote": true,
    "quoted_tweet": {
      "id": "1893400000000000000",
      "text": "Our latest launch was a success.",
      "author": {
        "userName": "SpaceX"
      }
    },
    "createdAt": "2026-02-24T15:10:00.000Z"
  }
}
```

### tweet.reply

A reply posted by the monitored account.

```json theme={null}
{
  "eventType": "tweet.reply",
  "username": "elonmusk",
  "data": {
    "id": "1893456789012345680",
    "text": "Great question. Working on it.",
    "author": {
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk"
    },
    "isRetweet": false,
    "isReply": true,
    "isQuote": false,
    "inReplyToId": "1893411111111111111",
    "createdAt": "2026-02-24T16:30:00.000Z"
  }
}
```

### tweet.retweet

A retweet posted by the monitored account.

```json theme={null}
{
  "eventType": "tweet.retweet",
  "username": "elonmusk",
  "data": {
    "id": "1893456789012345681",
    "text": "RT @xai: Exciting news today.",
    "author": {
      "id": "44196397",
      "userName": "elonmusk",
      "name": "Elon Musk"
    },
    "isRetweet": true,
    "isReply": false,
    "isQuote": false,
    "createdAt": "2026-02-24T17:00:00.000Z"
  }
}
```

### webhook.test

A test payload sent via the [Test Webhook](/api-reference/webhooks/test) endpoint to verify your endpoint is reachable.

```json theme={null}
{
  "eventType": "webhook.test",
  "data": {
    "message": "Test delivery from Xquik"
  },
  "timestamp": "2026-02-27T12:00:00.000Z"
}
```

## Retry policy

Failed deliveries are retried with exponential backoff (base 1 second, multiplier 2x, max 60 seconds):

<CardGroup cols={2}>
  <Card title="Attempts 1-3" icon="timer">
    Delays: 1 second, 2 seconds, then 4 seconds.
  </Card>

  <Card title="Attempts 4-6" icon="timer-reset">
    Delays: 8 seconds, 16 seconds, then 32 seconds.
  </Card>

  <Card title="Attempts 7-9" icon="hourglass">
    Delay: 60 seconds for each attempt after the cap is reached.
  </Card>

  <Card title="Attempt 10" icon="circle-stop">
    Final attempt. If it fails, the delivery is marked as `exhausted`.
  </Card>
</CardGroup>

After the 10th failed attempt, the delivery is marked as `exhausted`. A `410 Gone` response exhausts the delivery immediately. Check delivery status via the [deliveries endpoint](/api-reference/webhooks/deliveries).

Webhook configuration also tracks receiver health. `GET /webhooks` returns
`deliveryStatus`, `consecutiveFailures`, and `failureHardCap`. When repeated
receiver failures reach the hard cap, `deliveryStatus` becomes
`needs_attention`. Fix the receiver, then call
[Resume Webhook](/api-reference/webhooks/resume) to require a signed test before
delivery resumes.

<Note>
  Retries are processed in batches, so actual delay may be slightly longer than shown.
</Note>

<Warning>
  **Terminal failures:** Return `410 Gone` only when Xquik should stop retrying that delivery. Other non-`2xx` responses and network failures retry until the delivery is exhausted.
</Warning>

## Backfill after a receiver outage

Use this handoff after your receiver was down, returned repeated non-`2xx`
responses, or exhausted deliveries. Fix the receiver first, then use delivery
rows to identify failed attempts and stored event pages to rebuild downstream
work. Do not use `410 Gone` for temporary throttling; it exhausts that delivery.

```json theme={null}
{
  "record_type": "webhook_receiver_backfill",
  "webhook_id": "15",
  "delivery_log": "GET /api/v1/webhooks/15/deliveries",
  "resume_endpoint": "POST /api/v1/webhooks/15/resume",
  "event_backfill_endpoint": "GET /api/v1/events?limit=100&after={nextCursor}",
  "source_filter": "monitorId for account monitors, keywordMonitorId for keyword monitors",
  "join_key": "delivery.streamEventId == event.id",
  "resume_fields": [
    "deliveryId",
    "streamEventId",
    "status",
    "attempts",
    "lastStatusCode",
    "lastError",
    "deliveryStatus",
    "consecutiveFailures",
    "failureHardCap",
    "nextCursor"
  ],
  "stop_when": "hasMore is false",
  "handoff_state": "receiver_fixed_resume_webhook_page_events_join_deliveries"
}
```

Store `nextCursor` after every event page. Reprocess events whose `id` matches
failed or exhausted delivery `streamEventId` values, then leave delivered rows
alone. Scope event pages with `monitorId` for account monitors or
`keywordMonitorId` for keyword monitors when the source is known; omit both
only for all-monitor replay.

## Troubleshoot a delivery

Use this handoff when a receiver fails tests, repeats failures, or reaches an
`exhausted` delivery state.

```json theme={null}
{
  "record_type": "webhook_delivery_troubleshooting",
  "webhook_id": "15",
  "signed_test": "POST /api/v1/webhooks/15/test",
  "resume_endpoint": "POST /api/v1/webhooks/15/resume",
  "delivery_log": "GET /api/v1/webhooks/15/deliveries",
  "event_join": "GET /api/v1/events/{id}",
  "event_id_source": "streamEventId",
  "route_on": [
    "deliveryStatus",
    "consecutiveFailures",
    "failureHardCap",
    "status",
    "attempts",
    "lastStatusCode",
    "lastError",
    "createdAt",
    "deliveredAt"
  ],
  "test_payload_has_ids": false,
  "production_payload_ids": ["deliveryId", "streamEventId"],
  "handoff_state": "verify_signature_resume_if_needed_check_delivery_join_event"
}
```

<CardGroup cols={2}>
  <Card title="Signed receiver test" icon="flask-conical">
    Run [Test Webhook](/api-reference/webhooks/test) after changing endpoint
    code, secrets, firewall rules, or queue routing. Treat `success: true`
    with a `2xx` `statusCode` as receiver proof.
  </Card>

  <Card title="Signature and IDs" icon="shield-check">
    Verify `X-Xquik-Signature`, `X-Xquik-Timestamp`, and `X-Xquik-Nonce` on
    the raw request body. `webhook.test` omits `deliveryId` and
    `streamEventId`; production deliveries include both IDs.
  </Card>

  <Card title="Delivery triage" icon="activity">
    Check [List Deliveries](/api-reference/webhooks/deliveries) for `status`,
    `attempts`, `lastStatusCode`, `lastError`, `createdAt`, and
    `deliveredAt`. Page on `exhausted`, warn on repeated `failed`, and ignore
    `delivered`.
  </Card>

  <Card title="Resume gate" icon="rotate-ccw">
    If `deliveryStatus` is `needs_attention`, fix the receiver, then call
    [Resume Webhook](/api-reference/webhooks/resume). Delivery resumes only
    after the signed test succeeds.
  </Card>

  <Card title="Event context" icon="link">
    Join `streamEventId` to [Get Event](/api-reference/events/get) when the
    receiver owner needs the original monitor event, tweet fields, username, or
    keyword query that triggered the delivery.
  </Card>
</CardGroup>

## Requirements

* Endpoint **must** use HTTPS
* Endpoint **must not** resolve to a private or internal IP address (localhost, 10.x.x.x, 172.16-31.x.x, 192.168.x.x, 169.254.x.x)
* Endpoints should respond promptly with a `2xx` status code. Non-`2xx` responses count as failures and trigger retries

## Where to go next

<CardGroup cols={2}>
  <Card title="Webhook API reference" icon="book-open" href="/api-reference/webhooks/create">
    Create, list, update, deactivate, test, resume, and inspect webhook deliveries.
  </Card>

  <Card title="Signature Verification" icon="shield-check" href="/webhooks/verification">
    Verify HMAC-SHA256 signatures and implement idempotency.
  </Card>

  <Card title="Testing Webhooks" icon="flask-conical" href="/guides/webhook-testing">
    Test webhook delivery locally with tunnels and mock payloads.
  </Card>

  <Card title="MCP equivalent" icon="bot" href="/mcp/tools">
    Use `xquik.request('/api/v1/webhooks', ...)` for create, list, update, delete, and test.
  </Card>

  <Card title="Monitor setup" icon="radio" href="/api-reference/monitors/create">
    Create account monitors that emit events for webhook delivery.
  </Card>

  <Card title="Keyword monitor setup" icon="search" href="/api-reference/monitors/create-keyword">
    Create keyword monitors that emit matching tweet events for webhook delivery.
  </Card>
</CardGroup>
