API reference
Base URL https://www.threadcamp.com/v1. Authenticate with a Bearer key. See the quickstart to get started, or grab the OpenAPI spec.
Idempotency
Pass a client_id on inbox creation and email sends. A repeat request with the same client_id returns the original resource (HTTP 200) instead of creating a duplicate.
Request IDs
Every response carries an x-request-id header, echoed as request_id inside error bodies. Include it when contacting support.
Rate limits
Every response returns x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (Unix seconds). Exceed the ceiling and you get 429 rate_limit_error with a retry-after header — pace off remaining rather than waiting for the 429.
Plan limits
Your plan sets a monthly send allowance plus inbox and custom-domain ceilings. Hitting one returns 402 quota_exceeded with a message naming the actual limit. On Pro and Scale, sends past the monthly allowance are not blocked — they continue as billable overage and the response carries overage: true. Current allowances: Free 1,000/mo, 3 inboxes, 1 domain; Pro 20,000/mo, 50 inboxes, 10 domains; Scale 100,000/mo, unlimited inboxes, unlimited domains. Machine-readable at /pricing.json.
Account and API keys
Everything here works without a browser. POST /v1/account is unauthenticated and returns a usable key immediately, so an agent can go from nothing to sending without a human clicking anything.
/v1/accountCreate an account and its first API key. The key is returned exactly ONCE and cannot be recovered. Omit BOTH email and password for a machine account, authenticated only by its key - useful when the domain that would host a contact address does not exist yet. A machine account cannot sign in to the dashboard and has no password reset until you attach a real address; supplying only one of the two is a 400.
# A machine account: no email, no password.
curl https://www.threadcamp.com/v1/account \
-H "Content-Type: application/json" -d '{}'
# Or a normal account you can also sign into.
curl https://www.threadcamp.com/v1/account \
-H "Content-Type: application/json" \
-d '{ "email": "you@acme.com", "password": "at-least-8-chars", "key_mode": "live" }'{
"account": { "id": "user_...", "object": "account",
"email": "user_...@machine.threadcamp.invalid", "plan": "free" },
"api_key": { "id": "key_...", "mode": "test", "key": "sk_test_..." },
"machine_account": true,
"message": "Machine account: this API key is the only credential and cannot be recovered. Store it now."
}/v1/meWho this key belongs to, and what it can do. Worth calling at startup: it is how you prove you are holding the RIGHT product's key and that it is live rather than test, before sending anything a customer sees. domains and inboxes come back null (not empty) if the mail engine cannot be reached, because an empty list would read as a verification failure.
curl https://www.threadcamp.com/v1/me \
-H "Authorization: Bearer sk_live_..."{
"object": "identity",
"account": { "id": "user_...", "email": "hello@acme.com", "plan": "pro" },
"api_key": { "id": "key_...", "name": "Production", "mode": "live", "prefix": "sk_live_abc123" },
"plan": { "id": "pro", "limits": { "emailsPerMonth": 50000 }, "overage_per_1k": 1.2 },
"usage": { "emails_this_month": 812, "emails_included": 50000, "metered_overage": true },
"relay_domain": "relay.threadcamp.com",
"domains": [ { "name": "acme.com", "status": "verified", "sending_ready": true } ],
"inboxes": { "count": 3, "default": { "address": "hello@acme.com" } }
}/v1/accountChange the account's contact address. Use this (or set_as_account_email on POST /v1/inboxes) to move a machine account onto a real address once your domain is verified.
curl -X PATCH https://www.threadcamp.com/v1/account \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "hello@acme.com" }'{ "id": "user_...", "object": "account", "email": "hello@acme.com", "plan": "pro" }/v1/api-keysCreate another key. Body: name?, mode? (live | test, default test). The raw key is returned exactly once. GET /v1/api-keys lists safe metadata; PATCH renames; DELETE revokes. Secrets and modes are immutable.
curl https://www.threadcamp.com/v1/api-keys \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "CI", "mode": "test" }'{ "id": "key_...", "object": "api_key", "name": "CI",
"mode": "test", "prefix": "sk_test_abc123", "key": "sk_test_..." }Inboxes
Create and manage addressable inboxes. Inboxes mint on the shared relay.threadcamp.com domain by default; pass a verified custom domain for your own. When someone replies to mail from an inbox, the reply is ALWAYS stored and readable via /v1/messages - forward_to also sends it to a human, and callback_url also sends it to your app.
/v1/inboxesCreate an inbox. Body: display_name?, username?, domain?, autonomy?, callback_url?, forward_to?, set_as_account_email?. Returns webhook_secret ONCE (signs email.received deliveries to callback_url). forward_to sends replies on to an off-platform address - it is stored as a Route, so SRS rewriting and loop guards apply and an inbox can never accumulate two conflicting forwards. set_as_account_email adopts the new address as the account contact in the same call, which is how a machine account moves off its placeholder address once your domain is verified.
curl https://www.threadcamp.com/v1/inboxes \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "display_name": "Support Agent", "autonomy": "approve-first" }'{
"id": "inbox_...",
"object": "inbox",
"address": "support-agent@relay.threadcamp.com",
"display_name": "Support Agent",
"autonomy": "approve-first",
"callback_url": null,
"is_default": false,
"status": "active",
"webhook_secret": "whsec_..." // shown once
}/v1/inboxesList the caller's inboxes (your default inbox is created on first list).
curl https://www.threadcamp.com/v1/inboxes \
-H "Authorization: Bearer sk_live_..."{
"object": "list",
"data": [
{ "id": "inbox_...", "object": "inbox",
"address": "support-agent@relay.threadcamp.com",
"autonomy": "approve-first", "status": "active" }
]
}/v1/inboxes/:idFetch a single inbox by id.
curl https://www.threadcamp.com/v1/inboxes/inbox_123 \
-H "Authorization: Bearer sk_live_..."{
"id": "inbox_123",
"object": "inbox",
"address": "support-agent@relay.threadcamp.com",
"autonomy": "approve-first",
"callback_url": null,
"status": "active"
}/v1/inboxes/:idUpdate an inbox: status (active|paused), callback_url, display_name, autonomy.
curl -X PATCH https://www.threadcamp.com/v1/inboxes/inbox_123 \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "callback_url": "https://acme.com/hooks/email" }'{
"id": "inbox_123",
"object": "inbox",
"address": "support-agent@relay.threadcamp.com",
"callback_url": "https://acme.com/hooks/email",
"status": "active"
}/v1/inboxes/:idDelete an inbox. Your default inbox cannot be deleted (400).
curl -X DELETE https://www.threadcamp.com/v1/inboxes/inbox_123 \
-H "Authorization: Bearer sk_live_..."{ "id": "inbox_123", "object": "inbox", "deleted": true, "address": "support-agent@relay.threadcamp.com" }/v1/inboxes/:id/messagesList messages in an inbox. Optional ?direction=inbound|outbound.
curl "https://www.threadcamp.com/v1/inboxes/inbox_123/messages?direction=inbound" \
-H "Authorization: Bearer sk_live_..."{
"object": "list",
"inbox_id": "inbox_123",
"data": [
{ "id": "msg_...", "object": "message", "direction": "inbound",
"from": "noreply@service.com", "subject": "Your code is 123456",
"status": "received", "created_at": "..." }
]
}Messages, threads and search
Read mail: list across every inbox, fetch one message, pull a whole conversation in a single call, or search subject and body full-text.
/v1/messagesList messages across all your inboxes, newest first. Filters: inbox_id, status, limit (max 200).
curl "https://www.threadcamp.com/v1/messages?status=received&limit=20" \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [ { "id": "msg_123", "object": "message", ... } ] }/v1/threadsList conversations, most recent activity first. Filters: inbox_id, limit.
curl https://www.threadcamp.com/v1/threads \
-H "Authorization: Bearer sk_live_..."{
"object": "list",
"data": [
{ "id": "thr_123", "object": "thread", "subject": "Invoice question",
"message_count": 4, "state": "open", "last_message_at": "..." }
]
}/v1/threads/:idOne conversation with every message embedded, oldest first - the read to make before replying.
curl https://www.threadcamp.com/v1/threads/thr_123 \
-H "Authorization: Bearer sk_live_..."{
"id": "thr_123",
"object": "thread",
"subject": "Invoice question",
"message_count": 2,
"messages": [
{ "id": "msg_1", "object": "message", "direction": "inbound", ... },
{ "id": "msg_2", "object": "message", "direction": "outbound", ... }
]
}/v1/searchFull-text search across subject and body. `query` is required; filters: inbox_id, limit.
curl "https://www.threadcamp.com/v1/search?query=invoice" \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "query": "invoice", "data": [ { "id": "msg_123", ... } ] }/v1/attachments/:idSigned download URL for an attachment on one of your messages. Valid 5 minutes; fetch it directly rather than storing it.
curl https://www.threadcamp.com/v1/attachments/att_123 \
-H "Authorization: Bearer sk_live_..."{
"id": "att_123",
"object": "attachment",
"filename": "invoice.pdf",
"url": "https://...signed...",
"expires_in": 300
}/v1/messages/:idFetch one message, including thread_id, body and status.
curl https://www.threadcamp.com/v1/messages/msg_123 \
-H "Authorization: Bearer sk_live_..."{
"id": "msg_123",
"object": "message",
"thread_id": "thr_...",
"inbox_id": "inbox_123",
"direction": "inbound",
"from": "noreply@service.com",
"to": ["support-agent@relay.threadcamp.com"],
"cc": [],
"subject": "Your code is 123456",
"text": "Your one-time code is 123456.",
"html": null,
"labels": [],
"status": "received",
"scheduled_at": null,
"created_at": "..."
}Emails (sending)
Resend-compatible send. Accepts the Resend payload plus scheduled_at (ISO), client_id (idempotency), thread_id and in_reply_to_message_id for real replies, force_approval for a stricter per-send hold, and attachments ([{ filename, content_base64, content_type }], 10MB total). `from` must be one of your inbox addresses - an unknown sender is refused, never rewritten. Sends from an approve-first inbox are held for human approval. Sending limits are enforced per-inbox daily; exceeding a cap returns the platform's 402.
/v1/emailsSend Markdown. `markdown` is rendered into BOTH an inline-styled HTML part and a matching plain-text part, so a link keeps its destination whichever part the recipient's client shows - passing the same markdown as `text` yourself is how "[Reset your password](https://...)" reaches a real inbox verbatim. `button` adds a real clickable call to action (it requires `markdown`; sent without it the request is refused rather than silently dropped). Explicit `html` or `text` wins over the derived value. Default styling is brand-neutral: a white card on light grey, system fonts, no webfont import. Pass `theme` to override.
curl https://www.threadcamp.com/v1/emails \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reset-9f2c" \
-d '{
"from": "hello@acme.com",
"to": ["someone@example.com"],
"subject": "Reset your password",
"markdown": "Hi there.\n\nTap the button below. The link expires in an hour.",
"button": { "text": "Reset your password", "url": "https://acme.com/r/abc" }
}'{
"id": "msg_...",
"object": "message",
"status": "sent"
}
// With a sk_test_ key you get the RENDERED parts back instead of a delivery,
// so a rendering mistake is catchable before anyone receives it:
{
"status": "test", "delivered": false,
"html": "<div ...><a href=\"https://acme.com/r/abc\" ...>Reset your password</a>...",
"text": "Hi there.\n\nTap the button below. The link expires in an hour.\n\nReset your password: https://acme.com/r/abc"
}/v1/emailsSend or reply. Body: from (one of your inbox addresses), to, subject, text|html, cc?, bcc?, reply_to?, scheduled_at?, client_id?, thread_id?, in_reply_to_message_id?, force_approval?.
curl https://www.threadcamp.com/v1/emails \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"from": "support-agent@relay.threadcamp.com",
"to": ["customer@example.com"],
"subject": "Your receipt",
"text": "Thanks for your order.",
"scheduled_at": "2026-08-01T09:00:00Z",
"client_id": "receipt-8891"
}'// auto inbox, immediate -> 201
{ "id": "msg_...", "object": "message", "status": "sent",
"thread_id": "thread_...", "created_at": "..." }
// approve-first inbox -> 202
{ "id": "msg_...", "status": "pending_approval", "approval_id": "msg_..." }
// future scheduled_at -> 202
{ "id": "msg_...", "status": "scheduled" }
// replayed client_id -> 200 with "idempotent": true/v1/emails/batchSend up to 100 emails in one call. Body: a JSON array of email objects (or { emails: [...] }). Each item runs the same pipeline as /v1/emails.
curl https://www.threadcamp.com/v1/emails/batch \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '[
{ "from": "a@relay.threadcamp.com", "to": ["x@example.com"],
"subject": "Hi", "text": "One" },
{ "from": "a@relay.threadcamp.com", "to": ["y@example.com"],
"subject": "Hi", "text": "Two" }
]'{
"object": "list", "sent": 2, "failed": 0,
"data": [
{ "index": 0, "http_status": 201, "id": "msg_...", "status": "sent" },
{ "index": 1, "http_status": 201, "id": "msg_...", "status": "sent" }
]
}Domains
Add a custom domain, publish the DNS records, verify. Verification is REAL and GRANULAR: DKIM is polled at the platform and every other record is resolved live, so the response names which record is missing, wrong or in conflict rather than only saying pending. Inbound mail lands on mail.yourdomain.com by default, so your normal email is untouched.
/v1/domainsAdd a domain. Returns the DNS records to place at your DNS host. Optional inbound_host chooses which name receives replies (default mail.<name>); pass the root domain to opt into the root deliberately.
curl https://www.threadcamp.com/v1/domains \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "acme-agents.email" }'{
"id": "dom_...",
"object": "domain",
"name": "acme-agents.email",
"status": "pending",
"inbound_host": "mail.acme-agents.email",
"sending_ready": null, // null until you verify
"receiving_ready": null,
"dns_records": [
// THREE DKIM CNAMEs, one per SES signing token
{ "type": "CNAME", "name": "xxxx._domainkey.acme-agents.email",
"value": "xxxx.dkim.amazonses.com",
"purpose": "dkim", "required": true },
{ "type": "TXT", "name": "acme-agents.email",
"value": "v=spf1 include:amazonses.com ~all",
"purpose": "spf", "required": true,
"note": "If this domain already has a v=spf1 record, MERGE the include into it - a second v=spf1 record breaks SPF entirely." },
{ "type": "MX", "name": "mail.acme-agents.email",
"value": "inbound-smtp.eu-west-2.amazonaws.com",
"priority": 10, "purpose": "mx", "required": false,
"note": "Receives replies, on a dedicated subdomain so your normal mail is untouched. Optional: without it, replies fall back to the shared relay." },
{ "type": "TXT", "name": "_dmarc.acme-agents.email",
"value": "v=DMARC1; p=quarantine;",
"purpose": "dmarc", "required": false }
],
"created_at": "...",
"verified_at": null
}/v1/domainsList the caller's domains.
curl https://www.threadcamp.com/v1/domains \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "id": "dom_...", "object": "domain", "name": "acme-agents.email",
"status": "verified", "sending_ready": true, "receiving_ready": true,
"inbound_host": "mail.acme-agents.email", "verified_at": "..." }
] }/v1/domains/:idRead one domain and its records WITHOUT re-running verification. Use this when polling for readiness; /verify re-hits DNS on every call.
curl https://www.threadcamp.com/v1/domains/dom_123 \
-H "Authorization: Bearer sk_live_..."{ "id": "dom_123", "object": "domain", "status": "verified",
"sending_ready": true, "receiving_ready": true }/v1/domains/:id/verifyRun a real verification check. Read each record's status and found - that is what tells you WHICH record is wrong. `conflict` means publishing ours would break something. `error` means the lookup failed, which is not the same as the record being missing.
curl -X POST https://www.threadcamp.com/v1/domains/dom_123/verify \
-H "Authorization: Bearer sk_live_..."{
"id": "dom_123",
"object": "domain",
"status": "pending",
"sending_ready": false,
"receiving_ready": false,
"dns_records": [
{ "purpose": "dkim", "name": "xxxx._domainkey.acme-agents.email",
"status": "verified", "found": ["xxxx.dkim.amazonses.com"] },
{ "purpose": "dkim", "name": "yyyy._domainkey.acme-agents.email",
"status": "missing", "found": [] },
{ "purpose": "spf", "name": "acme-agents.email",
"status": "conflict",
"found": ["v=spf1 include:_spf.google.com ~all"],
"detail": "This domain already sends mail through another service. Add \"include:amazonses.com\" to the EXISTING record - do not publish a second v=spf1 record, which would break SPF entirely." },
{ "purpose": "mx", "name": "mail.acme-agents.email",
"status": "verified", "found": ["10 inbound-smtp.eu-west-2.amazonaws.com"] }
]
}/v1/domains/:idStop using a domain. The shared SES identity is deliberately left in place, because deleting it could break another tenant that registered the same domain.
curl -X DELETE https://www.threadcamp.com/v1/domains/dom_123 \
-H "Authorization: Bearer sk_live_..."{ "id": "dom_123", "object": "domain", "deleted": true }Routes (forwarding)
Catch-all and alias forwarding rules to an OFF-platform address. Forwarding is real: SRS envelope rewriting and loop guards are applied automatically so SPF and DMARC survive.
/v1/routesCreate a Route. Body: match, destination, inbox_id?. destination must be an off-platform address.
curl https://www.threadcamp.com/v1/routes \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"match": "*@acme-agents.email",
"destination": "team@acme.com"
}'{
"id": "route_...",
"object": "route",
"match": "*@acme-agents.email",
"destination": "team@acme.com",
"enabled": true,
"created_at": "..."
}/v1/routesList forwarding rules.
curl https://www.threadcamp.com/v1/routes \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "id": "route_...", "object": "route", "match": "*@acme-agents.email",
"destination": "team@acme.com", "enabled": true }
] }/v1/routes/:idEnable or disable a Route. Body: enabled (boolean).
curl -X PATCH https://www.threadcamp.com/v1/routes/route_123 \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "enabled": false }'{ "id": "route_123", "object": "route", "enabled": false, ... }/v1/routes/:idDelete a Route.
curl -X DELETE https://www.threadcamp.com/v1/routes/route_123 \
-H "Authorization: Bearer sk_live_..."{ "id": "route_123", "object": "route", "deleted": true }Webhooks (per-inbox)
Account-level webhooks are retired. Webhooks are per-inbox: set callback_url on an inbox and inbound mail POSTs an HMAC-signed payload there. The signing webhook_secret is returned ONCE at inbox creation. Events: email.received, email.received.platform_trigger, email.bounced, email.complained, mail.approval.resolved. Delivery is attempted 3 times with a 0s / 1s / 4s backoff and a 10s timeout; a 3xx counts as non-delivery.
/v1/inboxes/:idPoint an inbox's callback_url at your endpoint to start receiving email.received deliveries.
curl -X PATCH https://www.threadcamp.com/v1/inboxes/inbox_123 \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "callback_url": "https://acme.com/hooks/email" }'{
"id": "inbox_123",
"object": "inbox",
"callback_url": "https://acme.com/hooks/email",
"status": "active"
}X-ThreadCamp-SignatureEvery delivery is signed. Verify it against the RAW request body BEFORE parsing JSON: re-serialising a parsed object changes the bytes and the check will fail. The timestamp guards replay. @threadcamp/sdk exports verifyWebhook and parseWebhook, which implement all of this - prefer them to rolling your own.
import { createHmac, timingSafeEqual } from "node:crypto";
// Header: X-ThreadCamp-Signature: t=<unix seconds>,v1=<hex hmac-sha256>
// Signed payload: `${t}.${rawBody}` Key: the inbox's webhook_secret
function verify(rawBody, header, secret) {
const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? "");
if (!m) return false;
const t = Number(m[1]);
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // replay window
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(m[2], "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
// Or, with the SDK:
import { parseWebhook } from "@threadcamp/sdk";
const payload = await parseWebhook(rawBody, req.headers["x-threadcamp-signature"], secret);{
"event": "email.received",
"inbox_id": "inbox_123",
"thread_id": "thr_...",
"correlation_token": "a1b2c3...",
"from": "customer@example.com",
"to": "hello@acme.com",
"subject": "Re: Reset your password",
"text": "I did not request this",
"html": "...",
"message_id": "<...>",
"in_reply_to": "<...>",
"received_at": "..."
}Approvals (human-in-the-loop)
The queue behind approve-first inboxes. A held send waits here until a human approves (which executes the send) or rejects it.
/v1/approvalsList approvals. Optional ?status=pending.
curl "https://www.threadcamp.com/v1/approvals?status=pending" \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "object": "approval", "id": "...", "status": "pending", ... }
] }/v1/approvals/:idResolve a pending approval. Body: action (approve|reject). Approving executes the held send.
curl https://www.threadcamp.com/v1/approvals/apr_123 \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "action": "approve" }'{
"object": "approval",
"id": "apr_123",
"status": "approved"
}Suppressions
Addresses ThreadCamp will not send to. Hard bounces, complaints and unsubscribes land here automatically; you can add and remove entries yourself. Sending to a suppressed address is skipped rather than attempted, and the send response reports it.
/v1/suppressionsList suppressed addresses and why each one is suppressed.
curl https://www.threadcamp.com/v1/suppressions \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "address": "bounced@example.com", "reason": "bounce" },
{ "address": "left@example.com", "reason": "unsubscribe" }
] }/v1/suppressionsStop sending to an address.
curl https://www.threadcamp.com/v1/suppressions \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "address": "someone@example.com" }'{ "address": "someone@example.com", "reason": "manual" }/v1/suppressions/:addressAllow sending to a previously suppressed address again. Do this only when you know why it was suppressed: re-sending to a hard bounce or a complaint damages your sending reputation.
curl -X DELETE https://www.threadcamp.com/v1/suppressions/someone%40example.com \
-H "Authorization: Bearer sk_live_..."{ "deleted": true }Events
The inspectable activity log, DERIVED from your message records: each message becomes one event (message.received / message.pending_approval / message.sent). There is no separate event store - this is exactly your message history, shaped as events.
/v1/eventsList recent events (derived from messages). Optional ?limit=N (default 50, max 200).
curl "https://www.threadcamp.com/v1/events?limit=20" \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "id": "evt_msg_...", "object": "event", "type": "message.sent",
"data": { "message_id": "msg_...", "inbox_id": "inbox_...",
"subject": "...", "from": "...", "to": ["..."] },
"created_at": "..." }
] }Errors
Every non-2xx response has a stable, machine-parseable shape. Branch on error.type; show error.message to humans; log error.request_id. error.param names the offending field on validation errors.
{
"error": {
"type": "invalid_request_error",
"message": "`from` is required.",
"param": "from",
"request_id": "req_...",
"docs_url": "https://www.threadcamp.com/docs/api"
}
}| error.type | HTTP | When |
|---|---|---|
authentication_error | 401 | Missing or invalid API key. |
invalid_request_error | 400 | A required field is missing or malformed. |
not_found | 404 | The referenced resource does not exist. |
conflict | 409 | The resource already exists or is in a conflicting state. |
quota_exceeded | 402 | A plan limit was reached. |
rate_limit_error | 429 | Too many requests - back off and retry. |
service_unavailable | 503 | A dependency is temporarily unavailable. |
Ship your first call.
Try it with the public sandbox key, then sign up for a live key.