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.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.
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 /ordersbody shape (run a few real ones; watch the order move throughin_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.
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.
3. Reading errors
Every error response — 4xx and 5xx — uses the same envelope. Branch onerror.code. Don’t pattern-match error.message; the wording can change.
error.code:
- 4xx with
field→ fix the request body. Don’t retry. - 401 → check the bearer header / key.
- 403 → key lacks permission. Use a
fullkey. - 429 → wait
details.retry_after_seconds, then retry. - 5xx → retry with backoff. Open a ticket if it persists; include the
Peeker-Request-Id.
| Code | Status | What it means | What to do |
|---|---|---|---|
invalid_request | 400 | A field is missing, the wrong type, or invalid. error.field points to the offender. | Fix the body, resend. |
unauthorized | 401 | Key missing, malformed, or revoked. | Re-check Authorization header. |
forbidden | 403 | Key valid but doesn’t permit this action. | Use a full preset key. |
not_found | 404 | No matching record in your account. | Don’t retry on a different key — 404 is account-scoped. |
rate_limited | 429 | 600/min cap exceeded. | Wait details.retry_after_seconds, retry. |
order_shape_conflict | 400 | POST /orders body mixed bundle_id with google_licenses / microsoft_licenses. | Pick one — bundle OR custom licenses. |
domain_count_mismatch | 400 | Submitted domain count doesn’t match the order plan. | Send microsoft_licenses + ceil(google_licenses / 2) domains. |
domain_already_in_active_order | 400 | One or more domains are on another in-progress order or one scheduled to cancel. | Cancel the other order or pick different domains. |
domain_not_imported | 400 | Domain 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_supported | 400 | Premium domain submitted on the standard order flow. | Pick a non-premium domain or contact support for manual procurement. |
domain_not_usable_for_provider | 400 | Domain can’t host the requested provider (often Google vs Microsoft DNS conflict). | Switch the domain’s provider or pick a different domain. |
internal_error | 500 | Unexpected 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: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.
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 inaction_required status. They show up in three places:
- The status field on orders, domains, imports, and swaps —
status: "action_required". - The
pending_actions[]array onGET /orders/{id}. - A
*.action_requiredwebhook event — one per resource family.
| Reason | What’s blocking | How to resolve |
|---|---|---|
update_nameservers | The customer hasn’t pointed nameservers at the registrar Peeker requested. | Send the nameserver_groups from GET /domains/import/{id} to the customer. |
replace_domain | The domain can’t be provisioned and needs swapping. | Submit a POST /swaps for that domain. |
premium_domain_issue | A premium domain was submitted that can’t go through the standard flow. | Pick a non-premium alternative or contact support. |
profile_picture_resubmit | A 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_required | The profile picture URL isn’t publicly reachable (private / localhost). | Re-host on a public CDN and update the URL. |
provider_workspace_problem | The 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.- Verify the signature. Reject any request with a missing or invalid
Peeker-Signature(or stalePeeker-Timestamp, > 5 min skew). Code in Webhooks → Verify the signature. - Make the handler idempotent. Dedupe on
Peeker-Event-Idfor at least 24h. Concurrent provisioning means events arrive out of order or double up under retry. - Return 2xx fast. You have 10 seconds. Push slow work into a queue; respond first.
- Don’t trust event ordering. A
domain.connectedcan land before itsorder.in_progress. Readdata.*.statusto decide what to render.