Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.peeker.ai/llms.txt

Use this file to discover all available pages before exploring further.

Peeker fires signed JSON events whenever async work happens — orders, domains, imports, and swaps. Register one URL per partner org in the portal; we deliver each event up to 5 times over 24 hours with exponential backoff. Verify the Peeker-Signature header before trusting any payload, and make your handler idempotent — concurrent provisioning means events can arrive out of order.

Register a URL and secret

Open the Partner portal → Webhooks. Set:
  • URL — your HTTPS endpoint that returns 2xx within 10 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.
You get one URL per partner org — no per-environment routing. Use the event.data.api_key.environment field if you want to branch on live vs sandbox.

Headers we send

HeaderWhat it carries
Peeker-Event-IdStable per logical event. Use to dedupe on your side. Format: evt_01HZX….
Peeker-TimestampUnix timestamp (seconds) when Peeker signed the body.
Peeker-Signaturev1=<hex hmac-sha256>. Versioned so the algorithm can rotate.

Body envelope

Every event uses the same envelope. Only data changes shape per event type.
{
  "id": "evt_01HZX0EV1A2B3C4D5E6F7G8H",
  "type": "order.in_progress",
  "created_at": "2026-05-08T12:00:00Z",
  "data": {
    "order": {
      "id": "ord_01HZX0OR1A2B3C4D5E6F7G8H",
      "status": "in_progress"
    }
  }
}

Event catalog

All public event types you can receive.

Orders

EventFired when
order.in_progressOrder accepted; provisioning has started. There is no separate order.created.
order.completedEvery domain and inbox in the order is live. Payload is the full provisioned order.
order.failedProvisioning hit an unrecoverable error.
order.action_requiredOrder needs partner attention. data.pending_actions[] lists each open action.
order.cancel_scheduledCancellation accepted; finalizes after a short grace window.
order.cancelledCancellation finished. Domains released and free for a new order.

Domains

EventFired when
domain.connectedDomain fully provisioned with its mail provider.
domain.action_requiredDomain needs partner attention (e.g. nameservers not flipped).
domain.forwarding_updatedForwarding URL change applied.
domain.forwarding_failedForwarding URL change couldn’t be applied (DNS lookup failure, invalid target).

Domain imports

EventFired when
domain_import.completedEvery domain in the import job finished.
domain_import.failedImport failed before any domains connected.
domain_import.action_requiredCustomer hasn’t pointed nameservers yet.

Swaps

EventFired when
swap.createdPOST /swaps or POST /swaps/user_names accepted.
swap.in_progressSwap workers picked it up.
swap.completedNew domain is up; old one detached (or kept warm 14 days for premium).
swap.failedSwap hit an unrecoverable error.
swap.action_requiredSwap 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.
#!/usr/bin/env bash
# Verify a Peeker webhook signature. Pipe the request body to stdin.
#   ./verify.sh "$PEEKER_TIMESTAMP" "$PEEKER_EVENT_ID" "$PEEKER_SIGNATURE" "$PEEKER_SECRET" < body.json
set -euo pipefail
ts="$1"; event_id="$2"; sig_header="$3"; secret="$4"
body=$(cat)
provided="${sig_header#v1=}"

# Refuse stale timestamps (> 5 minutes skew).
now=$(date +%s)
if [ $((now - ts)) -gt 300 ] || [ $((ts - now)) -gt 300 ]; then
  echo "stale timestamp" >&2; exit 1
fi

expected=$(printf '%s.%s.%s' "$ts" "$event_id" "$body" \
  | openssl dgst -sha256 -hmac "$secret" -hex \
  | awk '{print $2}')

# Constant-time compare via length+xor in shell is fragile; use python for safety.
python3 -c "
import hmac, sys
provided, expected = sys.argv[1], sys.argv[2]
ok = hmac.compare_digest(provided, expected)
sys.exit(0 if ok else 1)
" "$provided" "$expected"
Same logic in Node.js for production handlers:
# Save as verify.js and require it from your webhook route.
node -e "
const crypto = require('crypto');
const ts = process.env.PEEKER_TIMESTAMP;
const id = process.env.PEEKER_EVENT_ID;
const sig = process.env.PEEKER_SIGNATURE;
const secret = process.env.PEEKER_SECRET;
const body = require('fs').readFileSync(0, 'utf8');

if (Math.abs(Date.now()/1000 - Number(ts)) > 300) { process.exit(1); }
const expected = crypto.createHmac('sha256', secret)
  .update(\`\${ts}.\${id}.\${body}\`).digest('hex');
const provided = sig.replace(/^v1=/, '');
process.exit(crypto.timingSafeEqual(
  Buffer.from(provided, 'hex'),
  Buffer.from(expected, 'hex')
) ? 0 : 1);
"

Retries and deadletter

Failed deliveries (non-2xx response or timeout) retry up to 5 times over 24 hours with exponential backoff. After the 5th failure the event is marked failed in the portal and stays in your audit log. Respond with any 2xx within 10 seconds to acknowledge. The portal shows every attempt with the response status and timestamp.

Make your handler idempotent

Concurrent provisioning means events for the same resource can arrive out of order — you might see domain.connected before order.in_progress if a worker races, or get the same event twice if our retry catches a slow 2xx. Two rules:
  1. Dedupe on Peeker-Event-Id. Store seen IDs for at least 24 hours.
  2. Use the payload, not the order of arrival. Read data.*.status to 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.
  • 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.
When you receive one, surface the resource as “needs attention” in your UI. The full reason list and resolution steps live in Best Practices → Handling pending actions.
Last modified on May 14, 2026