Register a URL and secret
Open the Partner portal → Webhooks. Set:- URL - your HTTPS endpoint that returns
2xxquickly. Peeker times out delivery attempts after 15 seconds. - Secret - Peeker generates one when you save the URL. You’ll see it once. Store it in your secrets manager and pass it to the verifier.
Headers we send
| Header | What it carries |
|---|---|
Peeker-Event-Id | Stable per logical event. Use to dedupe on your side. Format: evt_01HZX…. |
Peeker-Timestamp | Unix timestamp (seconds) when Peeker signed the body. |
Peeker-Signature | v1=<hex hmac-sha256>. Versioned so the algorithm can rotate. |
Body envelope
Every event uses the same envelope. Onlydata changes shape per event type.
Event catalog
All public event types you can receive.Orders
| Event | Fired when |
|---|---|
order.in_progress | Order accepted; provisioning has started. There is no separate order.created. |
order.completed | Every domain and inbox in the order is live. Payload is the full provisioned order. |
order.failed | Provisioning hit an unrecoverable error. |
order.action_required | Order needs partner attention. Fetch rows from GET /orders/pending. |
order.cancel_scheduled | Cancellation accepted; finalizes after a short grace window. |
order.cancelled | Cancellation finished. Domains released and free for a new order. |
Domains
| Event | Fired when |
|---|---|
domain.connected | Domain fully provisioned with its mail provider. |
domain.action_required | Domain needs partner attention (e.g. nameservers not flipped). |
domain.forwarding_updated | Forwarding URL change applied. |
domain.forwarding_failed | Forwarding URL change couldn’t be applied (DNS lookup failure, invalid target). |
Domain imports
| Event | Fired when |
|---|---|
domain_import.completed | Every domain in the import job finished. |
domain_import.failed | Import failed before any domains connected. |
domain_import.action_required | Customer hasn’t pointed nameservers yet. |
Swaps
| Event | Fired when |
|---|---|
swap.created | POST /swaps or POST /swaps/user_names accepted. |
swap.in_progress | Swap workers picked it up. |
swap.completed | New domain is up; old one detached (or kept warm 14 days for premium). |
swap.failed | Swap hit an unrecoverable error. |
swap.action_required | Swap needs partner attention. |
Verify the signature
Build the string<unix_seconds>.<event_id>.<json_body> and HMAC-SHA-256 it with your listener’s secret. Compare in constant time to the value after v1=. Reject any request whose Peeker-Timestamp is more than 5 minutes from your server’s clock.
Retries and deadletter
Failed deliveries (non-2xx response or timeout) retry up to 5 times over 24 hours. After the 5th failure, the event is markedfailed in the portal. Return any 2xx within 15 seconds to acknowledge; faster is better so retries do not pile up behind a slow handler.
The portal shows every attempt with the response status and timestamp.
Make your handler idempotent
Events for the same resource can arrive out of order, and retries can deliver the same event twice. Two rules:- Dedupe on
Peeker-Event-Id. Store seen IDs for at least 24 hours. - Use the payload, not the order of arrival. Read
data.statusto decide what to render - don’t treat the event sequence as a state machine.
The action_required family
Every resource family has an action_required event:
order.action_required- most often a Google profile picture URL is private or invalid; callGET /orders/pending?order_id=...for rows.domain.action_required- nameservers aren’t pointed yet, or the domain needs replacement.domain_import.action_required- same nameserver issue scoped to an import job.swap.action_required- swap is blocked on partner input.