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.

A short tour of the conventions that don’t fit on any one endpoint page. Read once before integrating; come back when you hit a 4xx you don’t 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. What to test in sandbox:
  • 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 next to the request URL, status code, and (for errors) the response body. When you escalate to support, this is the single value we look up to find your call in our trace store.

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.
The 12 codes in v1:
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.Wait details.retry_after_seconds, retry.
order_shape_conflict400POST /orders body mixed bundle_id with google_licenses / microsoft_licenses.Pick one — bundle OR custom licenses.
domain_count_mismatch400Submitted domain count doesn’t match the order plan.Send microsoft_licenses + ceil(google_licenses / 2) domains.
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/api/partner/v1/orders?per_page=25",
    "next":  "https://api.peeker.ai/api/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/api/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 POST /orders 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 order’s normalized shape: user, provider route, bundle/license counts, the domains[] set, and the users[] array (in order). Reorder users[] and you’ll get a new order — that’s intentional, since the order maps to inbox name assignments. After the 24-hour window, the same body creates a new order. The original is unaffected — GET /orders/{id} still returns it.

6. Handling pending actions

A pending action is a fulfillment blocker that needs partner or customer input. Until it’s resolved, the resource sits in action_required status. They show up in three places:
  • The status field on orders, domains, imports, and swaps — status: "action_required".
  • The pending_actions[] array on GET /orders/{id}.
  • A *.action_required webhook event — one per resource family.
The reasons currently emitted by Peeker, and how to resolve each:
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.
premium_domain_issueA premium domain was submitted that can’t go through the standard flow.Pick a non-premium alternative or contact support.
profile_picture_resubmitA Google profile picture URL came back invalid (bad format / unreachable).Update the user’s profile_picture_url to a valid public URL.
profile_picture_public_link_requiredThe profile picture URL isn’t publicly reachable (private / localhost).Re-host on a public CDN and update the URL.
provider_workspace_problemThe saved provider credentials can’t complete the action.Submit a corrected order or update the user’s provider route before future orders.

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 May 14, 2026