ThreadCamp
Start free
Reference

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.

POST/v1/inboxes

Create an inbox. Body: display_name?, username?, domain?, domain_id?, autonomy?, client_id?.

Request
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" }'
Response
{
  "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"
}
GET/v1/inboxes

List the caller's inboxes.

Request
curl https://threadcamp.vercel.app/v1/inboxes \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "object": "list",
  "data": [
    { "id": "inbox_...", "object": "inbox",
      "address": "support-agent@in.threadcamp.com",
      "autonomy": "approve-first", "created_at": "..." }
  ]
}
GET/v1/inboxes/:id

Fetch a single inbox by id.

Request
curl https://threadcamp.vercel.app/v1/inboxes/inbox_123 \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "id": "inbox_123",
  "object": "inbox",
  "address": "support-agent@in.threadcamp.com",
  "domain_id": null,
  "autonomy": "approve-first",
  "created_at": "..."
}
DELETE/v1/inboxes/:id

Delete an inbox.

Request
curl -X DELETE https://threadcamp.vercel.app/v1/inboxes/inbox_123 \
  -H "Authorization: Bearer sk_live_..."
Response
{ "id": "inbox_123", "object": "inbox", "deleted": true }
GET/v1/inboxes/:id/messages

List messages in an inbox. Optional ?direction=inbound|outbound.

Request
curl "https://threadcamp.vercel.app/v1/inboxes/inbox_123/messages?direction=inbound" \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "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.

GET/v1/messages/:id

Fetch one message, including thread_id, body and status.

Request
curl https://threadcamp.vercel.app/v1/messages/msg_123 \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "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.

POST/v1/emails

Send an email. Body: from, to, subject, text|html, cc?, scheduled_at?, client_id?.

Request
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"
  }'
Response
// 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": "..." }
POST/v1/emails/batch

Send 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.

Request
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" }
  ]'
Response
{
  "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.

POST/v1/domains

Add a domain. Returns the MX/SPF/DKIM/DMARC records to place.

Request
curl https://threadcamp.vercel.app/v1/domains \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "acme-agents.email" }'
Response
{
  "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
}
GET/v1/domains

List the caller's domains.

Request
curl https://threadcamp.vercel.app/v1/domains \
  -H "Authorization: Bearer sk_live_..."
Response
{ "object": "list", "data": [
  { "id": "dom_...", "object": "domain", "name": "acme-agents.email",
    "status": "verified", "verified_at": "..." }
] }
POST/v1/domains/:id/verify

Check DNS and mark the domain verified.

Request
curl -X POST https://threadcamp.vercel.app/v1/domains/dom_123/verify \
  -H "Authorization: Bearer sk_live_..."
Response
{
  "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.

POST/v1/routes

Create a Route. Body: match, action?, destination, srs?, domain_id?.

Request
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
  }'
Response
{
  "id": "route_...",
  "object": "route",
  "domain_id": null,
  "match": "*@acme-agents.email",
  "action": "forward",
  "destination": "team@acme.com",
  "srs": true,
  "created_at": "..."
}
GET/v1/routes

List forwarding rules.

Request
curl https://threadcamp.vercel.app/v1/routes \
  -H "Authorization: Bearer sk_live_..."
Response
{ "object": "list", "data": [
  { "id": "route_...", "object": "route", "match": "*@acme-agents.email",
    "action": "forward", "destination": "team@acme.com", "srs": true }
] }
DELETE/v1/routes/:id

Delete a Route.

Request
curl -X DELETE https://threadcamp.vercel.app/v1/routes/route_123 \
  -H "Authorization: Bearer sk_live_..."
Response
{ "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.

POST/v1/webhooks

Register an endpoint. Body: url, events? (defaults to ["*"]).

Request
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"] }'
Response
{
  "id": "wh_...",
  "object": "webhook",
  "url": "https://acme.com/hooks/threadcamp",
  "events": ["message.received", "message.sent"],
  "created_at": "...",
  "secret": "whsec_..."   // shown once
}
GET/v1/webhooks

List registered webhooks (secret not included).

Request
curl https://threadcamp.vercel.app/v1/webhooks \
  -H "Authorization: Bearer sk_live_..."
Response
{ "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.

GET/v1/approvals

List approvals. Optional ?status=pending.

Request
curl "https://threadcamp.vercel.app/v1/approvals?status=pending" \
  -H "Authorization: Bearer sk_live_..."
Response
{ "object": "list", "data": [
  { "id": "apr_...", "object": "approval", "message_id": "msg_...",
    "inbox_id": "inbox_...", "status": "pending",
    "created_at": "...", "resolved_at": null }
] }
POST/v1/approvals/:id

Resolve a pending approval. Body: action (approve|reject). Approving executes the held send.

Request
curl https://threadcamp.vercel.app/v1/approvals/apr_123 \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "action": "approve" }'
Response
{
  "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.

GET/v1/events

List recent events. Optional ?limit=N (default 50, max 200).

Request
curl "https://threadcamp.vercel.app/v1/events?limit=20" \
  -H "Authorization: Bearer sk_live_..."
Response
{ "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.typeHTTPWhen
authentication_error401Missing or invalid API key.
invalid_request_error400A required field is missing or malformed.
not_found404The referenced resource does not exist.
conflict409The resource already exists or is in a conflicting state.
quota_exceeded402A plan limit was reached.
rate_limit_error429Too many requests - back off and retry.
service_unavailable503A dependency is temporarily unavailable.

Ship your first call.

Try it with the public sandbox key, then sign up for a live key.

Start freeMCP server