Skip to main content
Read this once before integrating; come back when you hit an error or pending action you do not recognize.

1. Test in sandbox first

pk_test_… runs the full API surface against a sandbox database. Every shape matches live, every webhook fires, no provisioning runs against real domain registrars or mail providers, and no charges are created. Test these before going live:
  • Your POST /orders body shape (run a few real ones; watch the order move through in_progress → completed).
  • Your webhook signature verification - sandbox events are signed with the sandbox secret you configure separately.
  • Your error-recovery branches - sandbox returns the same error envelope as live for invalid bodies.
Switch to pk_live_… only after you’ve completed at least one full order in sandbox and verified the order/webhook flow end-to-end.

2. Always log Peeker-Request-Id

Every response - success or error - carries a Peeker-Request-Id header.
HTTP/1.1 200 OK
Peeker-Request-Id: req_01HZX0E5K7N9P2Q4R6S8T0U1V3
Log it with the request URL, status code, and error body. Support uses this ID to find the exact call.

3. Reading errors

Every error response - 4xx and 5xx - uses the same envelope. Branch on error.code. Don’t pattern-match error.message; the wording can change.
{
	"error": {
		"code": "domain_count_mismatch",
		"message": "Submitted domain count does not match the order plan",
		"field": "domains",
		"details": {
			"required_count": 4,
			"submitted_count": 2,
			"missing_count": 2
		}
	}
}
Decision rule. Look at the status, then error.code:
  • 4xx with field → fix the request body. Don’t retry.
  • 401 → check the bearer header / key.
  • 403 → key lacks permission. Use a full key.
  • 429 → wait details.retry_after_seconds, then retry.
  • 5xx → retry with backoff. Open a ticket if it persists; include the Peeker-Request-Id.
Common v1 codes:
CodeStatusWhat it meansWhat to do
invalid_request400A field is missing, the wrong type, or invalid. error.field points to the offender.Fix the body, resend.
unauthorized401Key missing, malformed, or revoked.Re-check Authorization header.
forbidden403Key valid but doesn’t permit this action.Use a full preset key.
not_found404No matching record in your account.Don’t retry on a different key - 404 is account-scoped.
rate_limited429600/min cap exceeded, or live synchronous import capacity is busy.Wait details.retry_after_seconds, retry. For bulk imports, use /domains/import/jobs.
order_shape_conflict400POST /orders body mixed bundle_id with custom license/density fields.Pick one - bundle OR custom sizing.
domain_count_mismatch400Submitted domain count doesn’t match the order plan.Match the resolved bundle plan, or your custom license/density fields.
domain_already_in_active_order400One or more domains are on another in-progress order or one scheduled to cancel.Cancel the other order or pick different domains.
domain_not_imported400Domain is registered with another registrar and hasn’t been imported for this customer.POST /domains/import first; wait for domain_import.completed.
premium_domain_not_supported400Premium domain submitted on the standard order flow.Pick a non-premium domain or contact support for manual procurement.
domain_not_usable_for_provider400Domain can’t host the requested provider (often Google vs Microsoft DNS conflict).Switch the domain’s provider or pick a different domain.
internal_error500Unexpected server error.Retry with exponential backoff. Persisting? File a ticket with the request ID.

4. Paging through lists

Every list endpoint returns the same envelope:
{
	"data": [],
	"links": {
		"first": "https://api.peeker.ai/partner/v1/orders?per_page=25",
		"next": "https://api.peeker.ai/partner/v1/orders?per_page=25&page_token=eyJ..."
	},
	"meta": { "path": "...", "per_page": 25, "returned": 25 }
}
The pattern: keep calling links.next until it’s null. Default page size is 25, max is 100. For backfills, request per_page=100; for UI lists, default 25 reads fastest first row.
url="https://api.peeker.ai/partner/v1/orders?per_page=100"
while [ -n "$url" ] && [ "$url" != "null" ]; do
  page=$(curl -sS -H "Authorization: Bearer $PEEKER_KEY" "$url")
  echo "$page" | jq -c '.data[]'
  url=$(echo "$page" | jq -r '.links.next')
done
page_token is signed and encodes the cursor and the filters from the original request. Don’t construct or decode it by hand. If you change filters mid-walk, restart from links.first - the existing token will return invalid_request with field: "page_token". There are no totals (total, total_count, etc.) in v1 - page to the end and accumulate if you need an exact count.

5. Retrying job creation is safe

Resend the same body and you get the same order back. We hash the order shape server-side and dedupe for 24 hours. No header to set, no idempotency key to generate. The fingerprint hashes the normalized order: user, provider route, bundle/custom sizing, inboxes-per-domain, domains, personas, and cost basis. Two custom orders with the same license counts but different domain density do not dedupe together. After the 24-hour window, the same body creates a new order. The original is unaffected - GET /orders/{id} still returns it. Async import jobs also dedupe retries without a header, using the normalized sorted unique domain set. Resending the same domains returns the same import job.

6. Handling pending actions

A pending action is a blocker that needs partner or customer input. Until it is resolved, the resource stays action_required. You see pending actions in three places:
  • The status field on orders, domains, imports, and swaps - status: "action_required".
  • The GET /orders/pending endpoint for order actions.
  • A *.action_required webhook event - one per resource family.
Public reasons:
ReasonWhat’s blockingHow to resolve
update_nameserversThe customer hasn’t pointed nameservers at the registrar Peeker requested.Send the nameserver_groups from GET /domains/import/{id} to the customer.
replace_domainThe domain can’t be provisioned and needs swapping.Submit a POST /swaps for that domain.
profile_picture_not_accessibleA Google profile picture URL is invalid, private, local, or unreachable.Re-host on a public CDN and place a corrected order.

7. Webhook handler hygiene

Four rules. All four matter on day one of production traffic.
  1. Verify the signature. Reject any request with a missing or invalid Peeker-Signature (or stale Peeker-Timestamp, > 5 min skew). Code in Webhooks → Verify the signature.
  2. Make the handler idempotent. Dedupe on Peeker-Event-Id for at least 24h. Concurrent provisioning means events arrive out of order or double up under retry.
  3. Return 2xx fast. You have 10 seconds. Push slow work into a queue; respond first.
  4. Don’t trust event ordering. A domain.connected can land before its order.in_progress. Read data.status to decide what to render.
Last modified on June 29, 2026