# Webhooks

Webhooks let Eraya notify your systems in real time when your A/B tests change state — created, started, paused, resumed, stopped, completed, when a test reaches statistical significance, or whenever its stats are recomputed. Instead of polling the dashboard, you give Eraya an HTTPS URL and we POST a signed JSON payload to it the moment something happens.

***

### What is a webhook?

A webhook is an HTTP callback. You register an endpoint URL once; Eraya sends an HTTP `POST` request to that URL every time an event you subscribed to occurs. Common uses:

* Post test results into Slack, Discord, or Microsoft Teams
* Trigger downstream workflows (data warehouse loads, BI refreshes)
* Alert your team the moment a test reaches significance
* Sync test lifecycle into your own project tracker

***

### Prerequisites

* An active Eraya account with a connected Shopify store
* A **Growth plan** subscription or higher (webhooks are gated to Growth+)
* A publicly reachable **HTTPS** endpoint that can accept `POST` requests

***

### Setting up a webhook

1. Go to **Settings → Webhooks** in the Eraya dashboard.
2. Click **+ Add webhook**.
3. Enter your **Endpoint URL** (must start with `https://`).
4. Optionally add a **description** (e.g. "Slack notifier for #experiments").
5. Select the **events** you want to receive.
6. Click **Create webhook**.

Eraya automatically generates a **signing secret** for the subscription. Reveal it from the webhook row and copy it into your receiver's configuration — you'll use it to verify that incoming requests genuinely came from Eraya.

Use the **Send test** button at any time to deliver a sample `test.ping` payload and confirm your endpoint is reachable.

***

### Event types

| Event                       | When it fires                                                                 |
| --------------------------- | ----------------------------------------------------------------------------- |
| `test.created`              | A new test is created (in any status, usually `draft`)                        |
| `test.started`              | A test moves from `draft` → `active`                                          |
| `test.resumed`              | A test moves from `paused` → `active`                                         |
| `test.paused`               | A test moves from `active` → `paused`                                         |
| `test.stopped`              | A test is manually completed by a user                                        |
| `test.completed`            | A test is completed automatically (e.g. by its scheduled end date)            |
| `test.reached_significance` | A variation crosses the statistical-significance threshold for the first time |
| `test.stats_updated`        | Test statistics were recomputed (**high volume** — see note below)            |

> **About `test.stats_updated`:** stats are recomputed on a daily schedule for every active test, plus on demand whenever a test's status, cost data, or window changes. Subscribing to this event means roughly one delivery per active test per day, plus extras. Only subscribe if you specifically need every recompute.

You can subscribe to specific events, or use `*` to receive all of them.

***

### Payload format

Every delivery has the same envelope. The `data` object varies by event type.

```json
{
  "id": "evt_5b58f53c-8e38-4726-bb41-612fb53fd6ab",
  "type": "test.resumed",
  "createdAt": "2026-05-16T02:18:32.489Z",
  "storeId": "69ec0570c48f9e90a9b642a0",
  "data": {
    "testId": "6a066fdc67cf643b6385a8d7",
    "testType": "splitUrl",
    "name": "Homepage hero test",
    "status": "active",
    "previousStatus": "paused",
    "startDate": "2026-05-15T00:59:08.091Z",
    "variations": [
      { "id": "a", "name": "Control", "percentage": 50, "isControl": true },
      { "id": "b", "name": "Variation 1", "percentage": 50, "isControl": false }
    ]
  }
}
```

For `test.reached_significance` and `test.stats_updated`, the `data` object additionally includes the computed `comparisons`, `variations`, `asOfDate`, and (for stats) `computeReason` and session counts.

#### Request headers

| Header              | Description                                                        |
| ------------------- | ------------------------------------------------------------------ |
| `X-Eraya-Event`     | The event type (e.g. `test.resumed`)                               |
| `X-Eraya-Delivery`  | Unique delivery ID — equals the `id` field; use it for idempotency |
| `X-Eraya-Signature` | HMAC signature of the raw body (see below)                         |
| `User-Agent`        | Always `Eraya-Webhooks/1.0`                                        |
| `Content-Type`      | `application/json`                                                 |

Your endpoint should respond with a `2xx` status code as quickly as possible. Any non-`2xx` response (or a timeout after 10 seconds) is treated as a failed delivery and retried.

***

### Verifying the signature

Every request includes an `X-Eraya-Signature` header of the form `sha256=<hex>`, where the value is an HMAC-SHA256 of the **raw request body** keyed with your subscription's signing secret.

**Important:** compute the HMAC over the raw, unparsed request body bytes. If you parse the JSON and re-serialize it before hashing, whitespace and key order will differ and the signature will not match.

#### Node.js (Express)

```js
const crypto = require('crypto')

// Capture the raw body before JSON parsing consumes it
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf }
}))

app.post('/webhooks/eraya', (req, res) => {
  const secret = process.env.ERAYA_WEBHOOK_SECRET // the whsec_... value
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.rawBody)
    .digest('hex')

  const got = req.header('X-Eraya-Signature') || ''
  const valid =
    got.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))

  if (!valid) return res.status(401).end()

  // Signature verified — process req.body
  res.status(200).end()
})
```

#### Python (Flask)

```python
import hmac, hashlib
from flask import request, abort

@app.post("/webhooks/eraya")
def eraya_webhook():
    raw = request.get_data()  # raw bytes
    expected = "sha256=" + hmac.new(
        SECRET.encode(), raw, hashlib.sha256
    ).hexdigest()
    got = request.headers.get("X-Eraya-Signature", "")
    if not hmac.compare_digest(got, expected):
        abort(401)
    # verified — handle request.json
    return "", 200
```

Always use a constant-time comparison (`timingSafeEqual` / `compare_digest`), not `==`.

***

### Delivery, retries, and failures

* **Timeout:** each attempt waits up to 10 seconds for a `2xx` response.
* **Retries:** failed deliveries are retried automatically with backoff, up to 5 attempts, before being moved to a dead-letter queue.
* **Idempotency:** retries reuse the same `X-Eraya-Delivery` / `id`. A subscription that already received a delivery successfully will not be POSTed to again for the same event. Use this ID to de-duplicate on your side.
* **Auto-disable:** if a subscription accumulates **20 consecutive failed deliveries**, Eraya automatically disables it. Re-enable it from the dashboard once your endpoint is healthy — this resets the failure counter.

***

### Delivery history

Open the **History** view on any webhook to see recent deliveries (retained for 30 days), including:

* Event type and timestamp
* Delivery status (`success`, `failed`, `dead`) and the HTTP status code returned
* The response body your endpoint returned (truncated)
* Any error message

You can **Redeliver** any past event manually — this re-queues the original payload for a fresh delivery attempt.

***

### Rotating the signing secret

From a webhook row, click **Rotate** to generate a new signing secret. The old secret is invalidated **immediately**, so update your receiver's configuration before the next event fires. Rotate the secret if you suspect it has been exposed.

***

### Best practices

* **Verify every request.** Reject anything whose signature doesn't match.
* **Respond fast.** Acknowledge with `2xx` immediately and do heavy processing asynchronously; slow endpoints get retried and may auto-disable.
* **Be idempotent.** Use `X-Eraya-Delivery` to ignore duplicates from retries.
* **Subscribe narrowly.** Only subscribe to the events you need — especially avoid `test.stats_updated` unless you require every recompute.
* **Monitor failures.** Watch the delivery history and the consecutive-failure count so you can fix issues before a subscription is auto-disabled.

***

### Troubleshooting

| Symptom                      | Likely cause                                                                                    |
| ---------------------------- | ----------------------------------------------------------------------------------------------- |
| No deliveries arriving       | Subscription is `disabled`, the store isn't on Growth+, or the event isn't subscribed           |
| Signature never matches      | Hashing a parsed/re-serialized body instead of the raw bytes, or using the wrong/rotated secret |
| Subscription keeps disabling | Endpoint returning non-`2xx` or timing out (>10s); check delivery history for the response code |
| Duplicate events             | Not de-duplicating on `X-Eraya-Delivery`; retries reuse the same delivery ID                    |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://eraya.gitbook.io/eraya-docs/integrate/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
