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

# Webhooks

> Receive signed lifecycle events from WPOS: a site connected, credits running low, a build job finished. Verify, handle, and debug them.

Webhooks push events to your endpoint the moment they happen, so you do not have to
poll. WPOS signs every delivery so you can prove it came from us. You set one
endpoint per partner, either in the [dashboard](/partners/dashboard) or with WPOS.

## Set your endpoint

In the Partner Dashboard, open **Settings**, then set your webhook URL. WPOS
generates a signing secret and shows it to you **exactly once**. Store it now: it
is never shown again, and changing the URL rotates it. The endpoint must be an
`https://` URL.

## Events

<AccordionGroup>
  <Accordion title="site.registered" icon="plug">
    A WordPress site connected to one of your managed accounts.

    ```json theme={null}
    {
      "event": "site.registered",
      "timestamp": "2026-08-08T...",
      "data": { "accountId": "acc_...", "externalRef": "ws_123", "siteId": "site_...", "siteUrl": "https://customer.com", "siteTitle": "Customer Co", "pluginVersion": "2.3.0" }
    }
    ```
  </Accordion>

  <Accordion title="credits.low" icon="battery-quarter">
    An account's balance crossed below ten percent of its plan's monthly credits.
    Fires once per billing cycle, so you can top the account up in time.

    ```json theme={null}
    {
      "event": "credits.low",
      "timestamp": "2026-08-08T...",
      "data": { "accountId": "acc_...", "externalRef": "ws_123", "balance": 45, "monthlyCredits": 500 }
    }
    ```
  </Accordion>

  <Accordion title="job.completed" icon="flag-checkered">
    A build job reached a terminal status. Fires on `succeeded`, `failed`,
    `timeout`, and `cancelled`, and carries the same result as the job poll.

    ```json theme={null}
    {
      "event": "job.completed",
      "timestamp": "2026-08-08T...",
      "data": { "jobId": "job_...", "externalRef": "job_001", "siteId": "site_...", "accountId": "acc_...", "status": "succeeded", "result": { "summary": "...", "changes": [], "warnings": [] }, "creditsConsumed": 6 }
    }
    ```
  </Accordion>
</AccordionGroup>

## The request

Every delivery is a `POST` to your URL with these headers:

| Header             | Value                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------- |
| `Content-Type`     | `application/json`                                                                     |
| `X-WPOS-Event`     | The event name, for example `job.completed`.                                           |
| `X-WPOS-Signature` | `sha256=<hex>`, an HMAC-SHA256 of the raw request body keyed with your webhook secret. |

The body is `{ "event": "...", "timestamp": "...", "data": { ... } }`.

## Verify the signature

Always verify before you trust a delivery. Compute the HMAC over the **raw** body
and compare it, in constant time, to the header.

```javascript theme={null}
import crypto from 'crypto';

// req.rawBody is the exact bytes WPOS sent. Do not re-serialize the parsed JSON.
function verifyWposWebhook(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');
  const a = Buffer.from(signatureHeader || '', 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

<Warning>
  Verify against the raw request body, not a re-serialized copy of the parsed JSON.
  If your framework parses the body before you see it, capture the raw bytes (for
  example Express's `express.json({ verify })` hook) so the HMAC matches.
</Warning>

## Retries and delivery

WPOS expects a `2xx` promptly. Deliveries time out after eight seconds. On any
failure or non-2xx, WPOS retries twice: about one minute later, then about five
minutes after that. After the third attempt the delivery is marked failed. Make
your handler idempotent: identify the work by `jobId` or `siteId` and tolerate a
repeat.

## Debug deliveries

The **Settings** page in the [dashboard](/partners/dashboard) shows a live log of
recent deliveries with the event, attempt count, outcome, and the failure reason
on any that did not land. Use it to confirm your endpoint is reachable and
returning 2xx.

## Next

<Card title="API reference" icon="code" href="/partners/api-reference">
  Every endpoint, scope, error, and limit in one place.
</Card>
