> ## 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.

# Test Twitter webhooks, signatures & retry events

> Test signed tweet, profile, and keyword monitor webhook deliveries with a local server, ngrok, or webhook.site. Verify HMAC signatures. Follow exact steps.

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

Test your webhook integration before deploying to production. Use Xquik's signed test delivery, local HTTPS tunnels, payload inspectors, and delivery logs to verify handlers before real monitor events arrive.

## Test with Xquik first

Use the [Test Webhook](/api-reference/webhooks/test) endpoint after you create a webhook. Xquik sends a real `webhook.test` delivery to the configured URL with the same `X-Xquik-Signature`, `X-Xquik-Timestamp`, and `X-Xquik-Nonce` headers used for production monitor events.

`webhook.test` payloads contain `eventType`, `data.message`, and `timestamp`.
They omit `deliveryId` and `streamEventId`. Use them for reachability and
signature checks, not production deduplication.

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

**Response when your endpoint accepts the delivery.**

```json theme={null}
{
  "success": true,
  "statusCode": 200
}
```

**Response when your endpoint rejects the delivery.**

```json theme={null}
{
  "success": false,
  "statusCode": 500,
  "error": "HTTP 500"
}
```

Check your server logs for the `webhook.test` payload. Verify the HMAC before processing it. Return a 2xx response only after your handler accepts the event.

## End-to-end handoff check

Use a signed test delivery to prove the receiver can verify requests. Then use
production monitor deliveries to prove idempotency and storage behavior.

```json theme={null}
{
  "workflow": "signed_webhook_test_handoff",
  "webhook": {
    "webhook_id": "15",
    "url": "https://example.com/xquik/webhook",
    "event_types": ["tweet.new", "tweet.reply"],
    "secret_storage": "store_once"
  },
  "test_delivery": {
    "endpoint": "/api/v1/webhooks/15/test",
    "event_type": "webhook.test",
    "has_delivery_id": false,
    "has_stream_event_id": false,
    "signature_headers": ["X-Xquik-Signature", "X-Xquik-Timestamp", "X-Xquik-Nonce"],
    "receiver_status": 200,
    "success": true
  },
  "production_delivery": {
    "event_type": "tweet.new",
    "delivery_id": "502",
    "stream_event_id": "9002",
    "dedupe_keys": ["deliveryId", "streamEventId"],
    "return_status": "2xx_before_slow_work"
  },
  "delivery_triage": {
    "endpoint": "/api/v1/webhooks/15/deliveries",
    "status": "failed",
    "attempts": 3,
    "last_status_code": 500,
    "last_error": "Internal Server Error"
  },
  "event_context": {
    "endpoint": "GET /api/v1/events/{id}",
    "id_source": "streamEventId",
    "store": ["monitorId", "monitorType", "type", "occurredAt", "data"]
  },
  "handoff_state": "verified_signature_then_join_event_context"
}
```

<CardGroup cols={2}>
  <Card title="Signature checkpoint" icon="shield-check">
    Store the webhook `secret` once. Verify the raw body with
    `X-Xquik-Signature`, `X-Xquik-Timestamp`, and `X-Xquik-Nonce` before
    parsing or queueing the event.
  </Card>

  <Card title="Test payload checkpoint" icon="braces">
    Treat `webhook.test` as a signed reachability check. It includes
    `eventType`, `data.message`, and `timestamp`, and omits `deliveryId` and
    `streamEventId`.
  </Card>

  <Card title="Production deduplication" icon="copy-check">
    Use `deliveryId` for receiver retry deduplication. Use `streamEventId` when one
    monitor event should process once across endpoint changes.
  </Card>

  <Card title="Delivery triage" icon="activity">
    Query `GET /api/v1/webhooks/{id}/deliveries` for `status`, `attempts`,
    `lastStatusCode`, `lastError`, `createdAt`, and `deliveredAt` before
    paging or replaying work.
  </Card>

  <Card title="Event join" icon="link">
    Use delivery `streamEventId` as the `{id}` for
    [Get Event](/api-reference/events/get). Store the event `monitorId`,
    `monitorType`, `type`, `occurredAt`, and `data` with the receiver incident.
  </Card>
</CardGroup>

### Receiver replay row

Store signed test deliveries separately from production delivery rows. A
`webhook.test` row proves reachability and HMAC verification only. Production
rows carry the IDs your receiver needs for retry deduplication, event deduplication, and
event detail joins.

```json theme={null}
{
  "record_type": "webhook_receiver_replay_check",
  "webhook_id": "15",
  "test_payload_has_ids": false,
  "production_delivery_id": "502",
  "production_stream_event_id": "9002",
  "signature_verified": true,
  "nonce_cache_key": "webhook:15:nonce_hash",
  "event_join": "GET /api/v1/events/9002",
  "delivery_log": "GET /api/v1/webhooks/15/deliveries",
  "shared_storage_excludes": [
    "endpoint_signing_values",
    "raw_request_body",
    "raw_signature",
    "full_headers"
  ]
}
```

Use this row after verification succeeds and before slow downstream work. Keep
the raw body available for the HMAC check, then store only the deduplication IDs,
join route, verification state, and sanitized delivery context in shared
systems.

## Local testing with ngrok

[ngrok](https://ngrok.com) creates a public HTTPS tunnel to your local server. Xquik can then deliver webhooks to your development machine.

<Steps>
  <Step title="Install ngrok">
    ```bash theme={null}
    # macOS
    brew install ngrok

    # Linux
    curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok-v3-stable-linux-amd64.tgz \
      | tar -xz -C /usr/local/bin

    # Authenticate (free account required)
    ngrok config add-authtoken YOUR_NGROK_TOKEN
    ```
  </Step>

  <Step title="Start your local server">
    Run your webhook handler on a local port such as `:3000`:

    ```bash theme={null}
    node server.js
    # or: python app.py
    # or: go run main.go
    ```
  </Step>

  <Step title="Start the ngrok tunnel">
    ```bash theme={null}
    ngrok http 3000
    ```

    ngrok outputs a public HTTPS URL:

    ```text theme={null}
    Forwarding  https://a1b2c3d4.ngrok-free.app → http://localhost:3000
    ```

    Copy the `https://` URL.
  </Step>

  <Step title="Create a webhook with the ngrok URL">
    ```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://a1b2c3d4.ngrok-free.app/webhook",
        "eventTypes": ["tweet.new", "tweet.reply"]
      }' | jq
    ```

    Save the `secret` from the response for signature verification.
  </Step>

  <Step title="Send a signed test delivery">
    Trigger a signed `webhook.test` request through the tunnel before waiting for real tweet events:

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

    Inspect your app logs and the ngrok web inspector at `http://localhost:4040`.
  </Step>

  <Step title="Trigger events">
    When a monitored account posts a tweet, Xquik delivers the event through ngrok to your local server. Check your server logs and the ngrok web inspector at `http://localhost:4040`.
  </Step>
</Steps>

<Warning>
  ngrok URLs change every time you restart the tunnel (free plan). Update your webhook URL after each restart, or use a paid ngrok plan for stable subdomains.
</Warning>

## Testing with webhook.site

Use [webhook.site](https://webhook.site) to inspect webhook payloads without running a local server.

<Steps>
  <Step title="Get a webhook.site URL">
    Visit [webhook.site](https://webhook.site). The site generates a unique HTTPS URL automatically:

    ```text theme={null}
    https://webhook.site/a1b2c3d4-e5f6-7890-abcd-ef1234567890
    ```
  </Step>

  <Step title="Create a webhook with the webhook.site URL">
    ```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://webhook.site/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "eventTypes": ["tweet.new"]
      }' | jq
    ```
  </Step>

  <Step title="View incoming payloads">
    When events arrive, they appear in the webhook.site dashboard in real time. Inspect headers (`X-Xquik-Signature`, `Content-Type`) and the JSON body to verify the payload format matches your expectations.
  </Step>
</Steps>

<Note>
  webhook.site is useful for inspecting payload structure. For testing signature verification and handler logic, use [ngrok with a local server](#local-testing-with-ngrok) instead.
</Note>

## Sending test payloads

Simulate a webhook delivery to your local handler without calling Xquik. This is useful for unit tests or offline debugging. For end-to-end verification of the configured webhook URL, prefer `POST /webhooks/{id}/test`.

```bash theme={null}
# Generate a test signature
SECRET="your_webhook_secret_here"
PAYLOAD=$(cat <<'JSON'
{"eventType":"tweet.new","schemaVersion":1,"deliveryId":"502","streamEventId":"9002","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"}}
JSON
)
TIMESTAMP=$(date +%s)000
NONCE=$(openssl rand -hex 16)

SIGNATURE="sha256=$(printf '%s.%s.%s' "$TIMESTAMP" "$NONCE" "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)"

# Send to your local server
curl -X POST http://localhost:3000/webhook \
  -H "Content-Type: application/json" \
  -H "X-Xquik-Timestamp: $TIMESTAMP" \
  -H "X-Xquik-Nonce: $NONCE" \
  -H "X-Xquik-Signature: $SIGNATURE" \
  -d "$PAYLOAD"
```

**Test payload structure.**

```json theme={null}
{
  "eventType": "tweet.new",
  "schemaVersion": 1,
  "deliveryId": "502",
  "streamEventId": "9002",
  "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"
  }
}
```

Include `deliveryId` and `streamEventId` in offline fixtures so receiver idempotency tests match production deliveries. Test other event types by changing the `eventType` field, for example to `tweet.reply`, `tweet.quote`, or `tweet.retweet`.

## Debugging delivery failures

### Check delivery status

Query the [deliveries endpoint](/api-reference/webhooks/deliveries) to see delivery attempts and error details:

```bash theme={null}
curl https://xquik.com/api/v1/webhooks/15/deliveries \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

**Response.**

```json theme={null}
{
  "deliveries": [
    {
      "id": "502",
      "streamEventId": "9002",
      "status": "failed",
      "attempts": 3,
      "lastStatusCode": 500,
      "lastError": "Internal Server Error",
      "createdAt": "2026-02-24T14:25:00.000Z"
    }
  ]
}
```

Join `streamEventId` to [Get Event](/api-reference/events/get) to find the
monitor event behind a failed delivery.

```bash theme={null}
curl https://xquik.com/api/v1/events/9002 \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

Store the event `monitorId`, `monitorType`, `type`, `occurredAt`, and `data`
with the delivery `id`, `status`, `attempts`, `lastStatusCode`, and
`lastError` before paging support, queue, or receiver owners.

### Reconcile missed deliveries

Use delivery rows for receiver attempts and event pages for stored monitor
history. The delivery `streamEventId` is the event `id`. A failed receiver
can rebuild its downstream queue from stored events after you fix the handler.

```json theme={null}
{
  "record_type": "webhook_delivery_reconciliation",
  "delivery_endpoint": "GET /api/v1/webhooks/15/deliveries",
  "event_backfill_endpoint": "GET /api/v1/events?limit=100&cursor={nextCursor}",
  "join_key": "delivery.streamEventId == event.id",
  "store": ["deliveryId", "streamEventId", "status", "attempts", "eventId", "nextCursor"],
  "next_action": "rebuild_receiver_queue_after_handler_fix"
}
```

Store `nextCursor` after each event page. Continue with
`GET /api/v1/events?limit=100&cursor={nextCursor}` until `hasMore` is `false`.
Then compare event IDs with delivery `streamEventId` values before replaying
your own downstream work.

### Delivery statuses

<CardGroup cols={2}>
  <Card title="pending" icon="clock">
    Not sent yet. It may wait while your endpoint is failing. Check recent
    deploys, tunnel uptime, and receiver availability.
  </Card>

  <Card title="delivered" icon="circle-check">
    Your endpoint returned `2xx`. Confirm your handler verified the signature
    and stored the event before it returned success.
  </Card>

  <Card title="failed" icon="triangle-alert">
    The latest attempt failed. Xquik retries with backoff until your endpoint
    returns `2xx`. Inspect `lastStatusCode`, `lastError`, and your receiver
    logs.
  </Card>

  <Card title="exhausted" icon="pause">
    The webhook was paused or deleted while this delivery waited. Resume it
    to receive waiting deliveries.
  </Card>
</CardGroup>

### Common failure reasons

<AccordionGroup>
  <Accordion title="Slow response">
    Respond within 10 seconds. If your handler is slow, return `200` immediately and process the event asynchronously using a background job queue.
  </Accordion>

  <Accordion title="Non-2xx response">
    Any response outside the 200-299 range counts as a failure. Check your server logs for unhandled exceptions or validation errors in your handler. Common causes include missing middleware such as `express.raw()`, JSON parse errors, or database connection failures.
  </Accordion>

  <Accordion title="DNS resolution failure">
    Xquik could not resolve the webhook URL hostname. Verify your domain DNS records are correct. If using ngrok, confirm the tunnel is still running.
  </Accordion>

  <Accordion title="TLS/SSL errors">
    Webhook URLs must use HTTPS with a valid certificate. Xquik rejects self-signed certificates. Use a trusted CA (Let's Encrypt, Cloudflare) or ngrok for local development.
  </Accordion>

  <Accordion title="Connection refused">
    The target server is not accepting connections. Verify your server is running and listening on the correct port. Check firewall rules if running on a cloud provider.
  </Accordion>

  <Accordion title="Signature verification failing">
    Compute the HMAC over the raw request body bytes, not a re-serialized JSON object. Re-serialization can alter whitespace or key ordering. Use `express.raw()` in Node.js, `request.get_data()` in Flask, or `io.ReadAll(r.Body)` in Go.
  </Accordion>
</AccordionGroup>

<CardGroup cols={3}>
  <Card title="Webhooks overview" icon="webhook" href="/webhooks/overview">
    How webhooks work, delivery format, and retry policy.
  </Card>

  <Card title="Signature verification" icon="shield-check" href="/webhooks/verification">
    HMAC-SHA256 verification in Node.js, Python, and Go.
  </Card>

  <Card title="Deliveries API" icon="truck" href="/api-reference/webhooks/deliveries">
    Query delivery attempts and statuses.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.