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

# X (Twitter) webhook idempotency & retry dedupe

> Handle repeated X (Twitter) webhook deliveries safely. Xquik resends the exact body on retry, so dedupe each event and keep your receiver idempotent every time.

> For the complete documentation index, see [llms.txt](https://docs.xquik.com/llms.txt).

This guide follows [webhook signature verification](https://docs.xquik.com/webhooks/verification.md). Verify each delivery first.

## Idempotency

Xquik retries failed webhook deliveries. Your endpoint may receive the same event multiple times. Each retry sends the first attempt's exact body. Only the signature headers change. Production monitor deliveries include `deliveryId` and `streamEventId`. Use `deliveryId` as the webhook delivery idempotency key. Use `streamEventId` when your system should process one monitor event only once. It applies across webhook retries or endpoint changes. Do not hash the raw request body when `deliveryId` is available.




`webhook.test` deliveries include `eventType`, `data`, and `timestamp`. They omit monitor idempotency fields. Use them to verify signatures and receiver reachability. Skip production event deduplication for the test request.

### Production store contract

Use a persistent store before acknowledging production events. Scope nonce,
delivery, and event keys by webhook ID. Retries for one endpoint then never block
another endpoint. Duplicate `deliveryId` or `streamEventId` checks should
return `2xx` after verification. The receiver has already accepted the
work.

```json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/bash.json","/languages/csharp.json","/languages/go.json","/languages/java.json","/languages/javascript.json","/languages/kotlin.json","/languages/php.json","/languages/python.json","/languages/ruby.json","/languages/typescript.json"]}}
{
  "record_type": "webhook_receiver_store_contract",
  "webhook_id": "15",
  "nonce_key": "webhook:15:nonce_hash",
  "nonce_ttl_seconds": 300,
  "delivery_key": "webhook:15:delivery:502",
  "event_key": "event:9002",
  "duplicate_delivery_status": 200,
  "duplicate_event_status": 200,
  "event_join": "GET /api/v1/events/9002",
  "ack_after": "signature_verified_and_queue_row_written",
  "ack_status": 202,
  "slow_work": "async_worker",
  "delivery_log": "GET /api/v1/webhooks/15/deliveries",
  "shared_storage_excludes": [
    "endpoint_signing_values",
    "raw_request_body",
    "raw_signature",
    "full_headers"
  ]
}
```

Keep raw request bytes only for signature verification. Store the nonce cache
marker, delivery key, event key, join route, and processing status. Keep them in
the shared table or queue. Return `2xx` only after verification and a durable
queue write. Move slow enrichment, exports, CRM sync, or alerting to an async
worker. The receiver can then finish within the 10-second delivery timeout.

```javascript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/bash.json","/languages/csharp.json","/languages/go.json","/languages/java.json","/languages/javascript.json","/languages/kotlin.json","/languages/php.json","/languages/python.json","/languages/ruby.json","/languages/typescript.json"]}}
const processedDeliveries = new Set();
const processedEvents = new Set();

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const payload = req.body.toString();

  if (!verifyWebhook(req, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(payload);

  if (event.eventType === "webhook.test") {
    return res.status(200).send("Test accepted");
  }

  if (typeof event.deliveryId !== "string" || typeof event.streamEventId !== "string") {
    return res.status(400).send("Missing idempotency fields");
  }

  if (processedDeliveries.has(event.deliveryId)) {
    return res.status(200).send("Delivery already processed");
  }

  processedDeliveries.add(event.deliveryId);

  if (processedEvents.has(event.streamEventId)) {
    return res.status(200).send("Event already processed");
  }

  processedEvents.add(event.streamEventId);
  handleEvent(event);

  res.status(200).send("OK");
});
```

```python Python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/bash.json","/languages/csharp.json","/languages/go.json","/languages/java.json","/languages/javascript.json","/languages/kotlin.json","/languages/php.json","/languages/python.json","/languages/ruby.json","/languages/typescript.json"]}}
processed_deliveries = set()
processed_events = set()

@app.route("/webhook", methods=["POST"])
def webhook():
    payload = request.get_data()

    if not verify_webhook(request, WEBHOOK_SECRET):
        return "Invalid signature", 401

    event = request.get_json()

    if event.get("eventType") == "webhook.test":
        return "Test accepted", 200

    delivery_id = event.get("deliveryId")
    stream_event_id = event.get("streamEventId")
    if not isinstance(delivery_id, str) or not isinstance(stream_event_id, str):
        return "Missing idempotency fields", 400

    if delivery_id in processed_deliveries:
        return "Delivery already processed", 200

    processed_deliveries.add(delivery_id)

    if stream_event_id in processed_events:
        return "Event already processed", 200

    processed_events.add(stream_event_id)

    handle_event(event)

    return "OK", 200
```

```go Go theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/bash.json","/languages/csharp.json","/languages/go.json","/languages/java.json","/languages/javascript.json","/languages/kotlin.json","/languages/php.json","/languages/python.json","/languages/ruby.json","/languages/typescript.json"]}}
type WebhookEvent struct {
	Data          map[string]any `json:"data"`
	DeliveryID    string         `json:"deliveryId"`
	EventType     string         `json:"eventType"`
	StreamEventID string         `json:"streamEventId"`
}

var (
	processedDeliveries sync.Map
	processedEvents     sync.Map
)

func processSignedWebhook(w http.ResponseWriter, r *http.Request) {
	payload, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}

	if !verifyWebhook(payload, r.Header, webhookSecret) {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}

	var event WebhookEvent
	if err := json.Unmarshal(payload, &event); err != nil {
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}

	if event.EventType == "webhook.test" {
		fmt.Fprint(w, "Test accepted")
		return
	}

	if event.DeliveryID == "" || event.StreamEventID == "" {
		http.Error(w, "Missing idempotency fields", http.StatusBadRequest)
		return
	}

	if _, replayed := processedDeliveries.LoadOrStore(
		event.DeliveryID,
		true,
	); replayed {
		fmt.Fprint(w, "Delivery already processed")
		return
	}

	if _, seen := processedEvents.LoadOrStore(
		event.StreamEventID,
		true,
	); seen {
		fmt.Fprint(w, "Event already processed")
		return
	}

	handleEvent(event)
	fmt.Fprint(w, "OK")
}
```

> [!WARNING]
>
> The in-memory examples above work for single-process servers. In production, use a persistent store such as a database or Redis. The store tracks processed delivery IDs and event IDs across restarts and multiple instances.

## Next steps

* [Signature verification](https://docs.xquik.com/webhooks/verification.md)
* [Webhooks overview](https://docs.xquik.com/webhooks/overview.md)


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