# Receive and verify webhooks

Get signed, real-time notifications (registrations, renewals, transfers, upcoming expirations, DNS changes) instead of polling — and prove each delivery really came from Porkbun.

## 1. Register an endpoint

```bash
curl -X POST https://api.porkbun.com/api/json/v3/webhook/create \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","url":"https://example.com/porkbun/webhook","events":["*"]}'
```

**Your endpoint URL must be publicly reachable HTTPS.** Specifically: `https://` scheme, the standard port 443, no username/password embedded in the URL, and a hostname that resolves to a public internet address. Private, loopback, link-local, CGNAT and other reserved addresses are refused with `INVALID_WEBHOOK_URL`, because Porkbun delivers from inside its own network. Registering a hostname that doesn't resolve yet is fine — that lets you create the endpoint before the receiver is deployed — but the same rules are re-checked immediately before every delivery, so an endpoint that later points at private space stops receiving events.

Testing locally? Use a tunnel that gives you a public HTTPS hostname (ngrok, Cloudflare Tunnel, tailscale funnel) rather than pointing the endpoint at localhost, or develop against a [sandbox key](https://porkbun.com/llms/sandbox) and fire events with `POST /sandbox/triggerWebhook`.

The response includes a **`secret`** — store it securely. It's the HMAC key you'll verify deliveries with. Omit `events` (or pass `["*"]`) for all event types; you can also subscribe to specific ones or a prefix wildcard like `dns.*`. Call `GET /webhook/eventTypes` for the catalog.

## 2. Verify every delivery

Each POST to your endpoint carries these headers:

- `X-Porkbun-Event` — the event type
- `X-Porkbun-Webhook-Id` — the event UUID (dedupe on this; an event may arrive more than once)
- `X-Porkbun-Webhook-Timestamp` — Unix seconds when it was signed
- `X-Porkbun-Signature` — `sha256=` + the signature

The signature is `HMAC-SHA256(secret, "{timestamp}.{rawBody}")` over the **raw request body** (verify before parsing JSON). Compare with a constant-time check and reject stale timestamps to blunt replays:

```php
$ts   = $_SERVER['HTTP_X_PORKBUN_WEBHOOK_TIMESTAMP'];
$sig  = $_SERVER['HTTP_X_PORKBUN_SIGNATURE'];          // "sha256=..."
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $endpointSecret);
if (!hash_equals($expected, $sig) || abs(time() - (int)$ts) > 300) { http_response_code(400); exit; }
// signature OK — now json_decode($body) and process
```

```javascript
import crypto from 'node:crypto';
const expected = 'sha256=' + crypto.createHmac('sha256', endpointSecret)
  .update(`${ts}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
  && Math.abs(Date.now()/1000 - Number(ts)) < 300;
```

Return any `2xx` to acknowledge. Non-2xx/timeout/connection errors are retried with exponential backoff (~1m, 5m, 30m, 2h, 6h, up to 6 attempts); 20 consecutive failures auto-disables the endpoint and emails you.

## 3. Test it end-to-end

```bash
curl -X POST https://api.porkbun.com/api/json/v3/webhook/test \
  -H 'Content-Type: application/json' \
  -d '{"apikey":"pk1_...","secretapikey":"sk1_...","id":42}'
```

This queues a `webhook.test` event so you can confirm your receiver and signature check work.

## 4. Inspect and replay

- `GET /webhook/deliveries` — recent delivery log (filter by `endpointId`/`status`), ~30-day history
- `GET /webhook/delivery/{id}` — one delivery with its full payload
- `POST /webhook/resend` `{id}` — re-queue a past delivery after you fix a bug (reuses the original event id, so your dedupe treats it as the same event)

## Troubleshooting a failing endpoint

`GET /webhook/get/{id}` and `GET /webhook/deliveries` report a `lastError`. An HTTP-level failure reads `HTTP 500` and so on — the status your server returned, which is what you need to debug it. A connection failure reads as a class instead ("The endpoint could not be reached.", "The endpoint did not respond in time.", "The endpoint TLS certificate could not be verified.", "The endpoint hostname could not be resolved."). Porkbun deliberately does not report the underlying socket error. After enough consecutive failures the endpoint is auto-disabled and the account owner is emailed; fix it and re-enable via `POST /webhook/update` with `"status":"ACTIVE"`.

Porkbun never follows redirects on a delivery — respond `2xx` directly from the URL you registered.

## Related

- Webhook endpoints: https://porkbun.com/llms/webhooks
- [Getting started](https://porkbun.com/llms/guides/getting-started)


---

## More

- Guides (how-tos): https://porkbun.com/llms/guides
- Topic index: https://porkbun.com/llms
- Full reference (one file): https://porkbun.com/llms-full.txt
- OpenAPI spec (full schemas): https://porkbun.com/api/json/v3/spec
- Short overview: https://porkbun.com/llms.txt
- Official MCP server: https://porkbun.com/mcp (`npx -y @porkbunllc/mcp-server`)
- Create API keys: https://porkbun.com/account/api
