API reference
Base URL https://threadcamp.vercel.app/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.
Inboxes
Create and manage addressable inboxes. Free inboxes mint on the shared in.threadcamp.com domain; pass a verified custom domain for your own.
/v1/inboxesCreate an inbox. Body: display_name?, username?, domain?, domain_id?, autonomy?, client_id?.
curl https://threadcamp.vercel.app/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@in.threadcamp.com",
"display_name": "Support Agent",
"domain_id": null,
"autonomy": "approve-first",
"created_at": "2026-07-12T09:00:00.000Z"
}/v1/inboxesList the caller's inboxes.
curl https://threadcamp.vercel.app/v1/inboxes \
-H "Authorization: Bearer sk_live_..."{
"object": "list",
"data": [
{ "id": "inbox_...", "object": "inbox",
"address": "support-agent@in.threadcamp.com",
"autonomy": "approve-first", "created_at": "..." }
]
}/v1/inboxes/:idFetch a single inbox by id.
curl https://threadcamp.vercel.app/v1/inboxes/inbox_123 \
-H "Authorization: Bearer sk_live_..."{
"id": "inbox_123",
"object": "inbox",
"address": "support-agent@in.threadcamp.com",
"domain_id": null,
"autonomy": "approve-first",
"created_at": "..."
}/v1/inboxes/:idDelete an inbox.
curl -X DELETE https://threadcamp.vercel.app/v1/inboxes/inbox_123 \
-H "Authorization: Bearer sk_live_..."{ "id": "inbox_123", "object": "inbox", "deleted": true }/v1/inboxes/:id/messagesList messages in an inbox. Optional ?direction=inbound|outbound.
curl "https://threadcamp.vercel.app/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
Fetch a single message (any direction) by id.
/v1/messages/:idFetch one message, including thread_id, body and status.
curl https://threadcamp.vercel.app/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@in.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) and client_id (idempotency). Sends from an approve-first inbox are held for human approval.
/v1/emailsSend an email. Body: from, to, subject, text|html, cc?, scheduled_at?, client_id?.
curl https://threadcamp.vercel.app/v1/emails \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"from": "support-agent@in.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",
"provider": "ses", "created_at": "..." }
// approve-first inbox -> 202
{ "id": "msg_...", "status": "pending_approval", "approval_id": "apr_..." }
// future scheduled_at -> 202
{ "id": "msg_...", "status": "scheduled", "scheduled_at": "..." }/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://threadcamp.vercel.app/v1/emails/batch \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '[
{ "from": "a@in.threadcamp.com", "to": ["x@example.com"],
"subject": "Hi", "text": "One" },
{ "from": "a@in.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, receive the full DNS record set, and verify it. Verified domains can host inboxes and send.
/v1/domainsAdd a domain. Returns the MX/SPF/DKIM/DMARC records to place.
curl https://threadcamp.vercel.app/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",
"dns_records": [
{ "type": "MX", "name": "acme-agents.email",
"value": "inbound-smtp.eu-west-2.amazonaws.com",
"priority": 10, "purpose": "receiving" },
{ "type": "TXT", "name": "acme-agents.email",
"value": "v=spf1 include:amazonses.com ~all", "purpose": "spf" },
{ "type": "CNAME", "name": "sf1._domainkey.acme-agents.email",
"value": "sf1.dkim.in.threadcamp.com", "purpose": "dkim" }
],
"created_at": "...",
"verified_at": null
}/v1/domainsList the caller's domains.
curl https://threadcamp.vercel.app/v1/domains \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "id": "dom_...", "object": "domain", "name": "acme-agents.email",
"status": "verified", "verified_at": "..." }
] }/v1/domains/:id/verifyCheck DNS and mark the domain verified.
curl -X POST https://threadcamp.vercel.app/v1/domains/dom_123/verify \
-H "Authorization: Bearer sk_live_..."{
"id": "dom_123",
"object": "domain",
"name": "acme-agents.email",
"status": "verified",
"verified_at": "..."
}Routes (forwarding)
Catch-all and alias forwarding rules. action is forward, webhook, inbox or split; srs rewrites the envelope sender so SPF and DMARC survive.
/v1/routesCreate a Route. Body: match, action?, destination, srs?, domain_id?.
curl https://threadcamp.vercel.app/v1/routes \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"match": "*@acme-agents.email",
"action": "forward",
"destination": "team@acme.com",
"srs": true
}'{
"id": "route_...",
"object": "route",
"domain_id": null,
"match": "*@acme-agents.email",
"action": "forward",
"destination": "team@acme.com",
"srs": true,
"created_at": "..."
}/v1/routesList forwarding rules.
curl https://threadcamp.vercel.app/v1/routes \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "id": "route_...", "object": "route", "match": "*@acme-agents.email",
"action": "forward", "destination": "team@acme.com", "srs": true }
] }/v1/routes/:idDelete a Route.
curl -X DELETE https://threadcamp.vercel.app/v1/routes/route_123 \
-H "Authorization: Bearer sk_live_..."{ "id": "route_123", "object": "route", "deleted": true }Webhooks
Register endpoints for signed event deliveries. The signing secret is returned once on create. Signature = sha256(secret + "." + body), sent as x-threadcamp-signature.
/v1/webhooksRegister an endpoint. Body: url, events? (defaults to ["*"]).
curl https://threadcamp.vercel.app/v1/webhooks \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "url": "https://acme.com/hooks/threadcamp",
"events": ["message.received", "message.sent"] }'{
"id": "wh_...",
"object": "webhook",
"url": "https://acme.com/hooks/threadcamp",
"events": ["message.received", "message.sent"],
"created_at": "...",
"secret": "whsec_..." // shown once
}/v1/webhooksList registered webhooks (secret not included).
curl https://threadcamp.vercel.app/v1/webhooks \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "id": "wh_...", "object": "webhook",
"url": "https://acme.com/hooks/threadcamp",
"events": ["message.received", "message.sent"], "created_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://threadcamp.vercel.app/v1/approvals?status=pending" \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "id": "apr_...", "object": "approval", "message_id": "msg_...",
"inbox_id": "inbox_...", "status": "pending",
"created_at": "...", "resolved_at": null }
] }/v1/approvals/:idResolve a pending approval. Body: action (approve|reject). Approving executes the held send.
curl https://threadcamp.vercel.app/v1/approvals/apr_123 \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "action": "approve" }'{
"id": "apr_123",
"object": "approval",
"message_id": "msg_...",
"status": "approved",
"resolved_at": "...",
"message_status": "sent"
}Events
The inspectable log of everything that happened on your account.
/v1/eventsList recent events. Optional ?limit=N (default 50, max 200).
curl "https://threadcamp.vercel.app/v1/events?limit=20" \
-H "Authorization: Bearer sk_live_..."{ "object": "list", "data": [
{ "id": "evt_...", "object": "event", "type": "message.sent",
"data": { "message_id": "msg_...", "provider": "ses" },
"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://threadcamp.vercel.app/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.