# Webhooks

> Get a signed POST on your server when something changes in your workspace. Add an endpoint, verify each delivery, and handle retries.
> Source: https://playcode.io/docs/developers/webhooks - last reviewed 2026-10-10.

A webhook is a signed POST that Playcode sends to your server when something happens in your workspace: a domain changed, a publish started, an email was delivered or bounced. You add the endpoint on the **Developers** page and verify each delivery with its signing secret.

## Do it yourself

1. Open **Settings** and choose **Developers** under the workspace.
2. Under **Webhooks**, choose **Add endpoint**.
3. In **URL**, enter a public `https` address on your server.
4. In **Which events**, enter exact event types or a family with a star, like `domain.* mail.*`. Leave it empty to get every event.
5. Choose **Add endpoint**. If Playcode asks you to confirm it's you, enter the code from the email it sends.
6. Copy the signing secret: "Copy it now - you won't see this token again." It starts with `pcwh_`. Keep it on your server, for example as `PLAYCODE_WEBHOOK_SECRET`.
7. On your server, verify each delivery and answer with a 2xx status within 10 seconds:

   ```ts
   import { verifyWebhook } from '@playcode/sdk'

   const { id, event } = verifyWebhook({
     signature: req.headers['playcode-signature'],
     body: rawBody, // the body exactly as it arrived
     secret: process.env.PLAYCODE_WEBHOOK_SECRET!,
   })
   ```

8. Check the deliveries: choose **Deliveries** on the endpoint's row. Each one shows its event, its status and your server's answer. **Send again** runs a failed delivery now.

### What a delivery looks like

| Header | Value |
|---|---|
| `Playcode-Signature` | `t=<unix seconds>,v1=<hex>` |
| `Playcode-Delivery` | The delivery's id, the same as `id` in the body |
| `Playcode-Event` | The event type, like `domain.updated` |
| `Content-Type` | `application/json` |

```json
{
  "id": "<delivery id>",
  "event": {
    "uid": "<event id>",
    "type": "domain.updated",
    "subjectType": "domain",
    "subjectUid": "<the domain's id>",
    "workspaceUid": "<workspace id>",
    "via": "api",
    "data": { "args": { "uid": "<the domain's id>", "input": { "autoRenew": true } } },
    "previous": { "autoRenew": false },
    "createdAt": "2026-10-10T09:00:00.000Z"
  }
}
```

The event types and their fields: see [Activity](/docs/developers/activity).

### Verify without the SDK

Compute HMAC-SHA256 with the secret over the text `t`, a dot, and the raw body; compare its hex with `v1` in constant time. Refuse a delivery whose `t` is more than 300 seconds from now: the SDK does the same.

## If it doesn't work

### "A webhook URL is https."

Use an `https` address. Plain `http` is refused.

### "... resolves to ..., which is not a public address." or "... does not resolve."

The address must resolve to a public IP address. Playcode checks it again before every delivery.

### "A webhook URL carries no credentials."

Remove the user name and password from the address. Check the signature instead.

### "A workspace holds at most 20 webhook endpoints; delete one first."

Remove an endpoint you no longer use.

### A delivery shows failed

Its row says why: "the endpoint answered" with your server's status, "no answer" when it did not answer within 10 seconds, or "redirects are never followed". Fix the server, then choose **Send again**.

### verifyWebhook throws "the signature does not match this body and secret"

Pass the raw body, before any JSON parsing, and the secret of this endpoint.

### verifyWebhook throws "the delivery is ... s old, past the 300 s tolerance"

Your server's clock is off, or you checked an old delivery. Every attempt is signed again with a fresh time.

### You lost the signing secret

The **Developers** page has no rotate button yet. Ask the agent in any project chat in the workspace to rotate the secret, or remove the endpoint and add it again: the new endpoint gets a new secret.

## Limits

- Up to 20 endpoints per workspace. Any member of the workspace can add one, and an endpoint gets every matching event of the whole workspace.
- Playcode tries a delivery up to 8 times: at once, then after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 8 hours and 24 hours.
- An endpoint that fails 20 times in a row, with no success for 3 days, gets no new deliveries until one succeeds. **Send again** tries one.
- Deliveries can arrive out of order, and the same delivery can arrive twice: deduplicate on `Playcode-Delivery`.
- Redirects are never followed. Each attempt waits 10 seconds for an answer.
- The delivery log keeps 30 days.
- A publish's result is not an event: `projectPublish.created` marks the start. Poll `projectPublishGet` for the end.
- Access tokens cannot manage webhooks. A signed-in person manages them in the app: on the **Developers** page, or by asking the agent in a chat.
