Skip to main content

Unified Inbox

Once a sequence is sending, replies land in a unified inbox. A background classifier tags each inbound reply (interested, not_now, objection, ooo, unsubscribe, bounce, other), groups it into a thread keyed on (sender, subject), and exposes the result through these endpoints. Use them to power a triage UI, build dashboard counts, draft replies via your own LLM key, or run automation on freshly-classified threads.

tip

Inbox endpoints require the Starter plan or higher. The classifier worker runs every few minutes; new replies typically appear in the inbox within ~5 minutes of arrival.

List Threads

List threads filtered by status, classification, assignee, or domain. Results are sorted by last_received_at descending.

Endpoint

GET /v1/inbox/threads

Headers

HeaderValue
AuthorizationBearer cdb_your_api_key

Query Parameters

ParameterTypeRequiredDescription
statusstringNoopen, handled, or snoozed. When open is requested, snoozed threads whose window has elapsed are auto-included.
classificationstringNointerested, not_now, objection, ooo, unsubscribe, bounce, other.
assigned_tointegerNoFilter by assignee. Pass 0 for "unassigned" (NULL).
domainstringNoRestrict to threads from a single domain.
limitintegerNoPage size (default 50, max 200).
offsetintegerNoSkip N (default 0, max 10000).

Example Request

curl -X GET \
-H "Authorization: Bearer cdb_your_api_key_here" \
"https://api.crondb.com/v1/inbox/threads?status=open&classification=interested&limit=20"

Python

import requests

response = requests.get(
"https://api.crondb.com/v1/inbox/threads",
headers={"Authorization": "Bearer cdb_your_api_key_here"},
params={"status": "open", "classification": "interested", "limit": 20},
)

data = response.json()
for t in data["items"]:
print(f"{t['contact_email']} ({t['domain']}) — {t['last_classification']}")

Node.js

const response = await fetch(
"https://api.crondb.com/v1/inbox/threads?status=open&classification=interested",
{ headers: { Authorization: "Bearer cdb_your_api_key_here" } }
);

const data = await response.json();
data.items.forEach((t) => console.log(`${t.contact_email}${t.last_classification}`));

Response

{
"total": 42,
"limit": 20,
"offset": 0,
"items": [
{
"id": 1284,
"thread_id": "a1b2c3d4e5",
"domain": "acme.com",
"contact_email": "jordan@acme.com",
"subject": "Re: Quick question about your pipeline",
"last_received_at": "2026-05-05T14:22:11Z",
"last_classification": "interested",
"last_classification_confidence": 0.91,
"message_count": 2,
"status": "open",
"assigned_to_user_id": null,
"notes": null,
"snoozed_until": null,
"created_at": "2026-05-05T08:14:02Z",
"updated_at": "2026-05-05T14:22:11Z"
}
]
}

Response Fields

FieldTypeDescription
totalintegerTotal threads matching the filters (ignores limit/offset).
items[].idintegerInternal primary key — pass to GET /threads/{id}.
items[].thread_idstringStable hash key joining replies into the same thread across send accounts.
items[].last_classificationstringLatest reply's classification.
items[].last_classification_confidencenumber0.0–1.0; 1.0 indicates a manual override.
items[].statusstringopen, handled, or snoozed.
items[].assigned_to_user_idinteger|nullOrg member assigned to handle this thread.
items[].snoozed_untilstring|nullWhen a snoozed thread will resurface as open.

Get Thread Counts

Sidebar/badge counts grouped by status and classification. Pivot dimensions exclude their own filter — useful for showing "how many would be in each bucket if I switched to it."

Endpoint

GET /v1/inbox/threads/counts

Query Parameters

ParameterTypeRequiredDescription
statusstringNoSame values as list.
classificationstringNoSame values as list.
assigned_tointegerNo0 = unassigned.
domainstringNoLimit to one domain.

Example Request

curl -X GET \
-H "Authorization: Bearer cdb_your_api_key_here" \
"https://api.crondb.com/v1/inbox/threads/counts?domain=acme.com"

Response

{
"by_status": {
"open": 18,
"handled": 23,
"snoozed": 1
},
"by_classification": {
"interested": 7,
"not_now": 9,
"ooo": 2,
"objection": 4,
"unsubscribe": 3,
"bounce": 1,
"other": 16
},
"total": 42
}

by_status ignores any status filter you passed (so the chips reflect counts under the rest of your filters); by_classification ignores any classification filter; total respects every filter.


Get Thread

Retrieve full message history for a thread, including each reply and its latest classification.

Endpoint

GET /v1/inbox/threads/{id}

Path Parameters

ParameterTypeRequiredDescription
idintegerYesThread primary key from the list response.

Example Request

curl -X GET \
-H "Authorization: Bearer cdb_your_api_key_here" \
"https://api.crondb.com/v1/inbox/threads/1284"

Response

{
"thread": {
"id": 1284,
"thread_id": "a1b2c3d4e5",
"domain": "acme.com",
"contact_email": "jordan@acme.com",
"subject": "Re: Quick question about your pipeline",
"last_classification": "interested",
"status": "open",
"message_count": 2
},
"replies": [
{
"id": 4421,
"from_email": "jordan@acme.com",
"from_name": "Jordan Lee",
"subject": "Re: Quick question about your pipeline",
"body_text": "Sounds interesting — can you share a deck?",
"received_at": "2026-05-05T14:22:11Z",
"message_id": "<CAJrf...@mail.gmail.com>",
"in_reply_to": "<20260505...@yourdomain.com>",
"domain": "acme.com",
"sequence_step_id": 87,
"classification": {
"label": "interested",
"confidence": 0.91,
"manually_overridden_to": null,
"model": "deepseek-chat",
"provider": "deepseek",
"classified_at": "2026-05-05T14:24:18Z"
}
}
]
}

Spoofing protection

The inbound reply fetcher parses SPF/DKIM Authentication-Results headers on every fetch. Replies that fail authentication and whose apparent sender domain doesn't match the connecting MTA are flagged internally as "spoofing suspected" — the classifier worker then defers classifying them, which means a spoofed From: contact@ won't auto-pause the sequence or mark the contact as interested. No action is needed on your end; this is automatic for all connected mailboxes.


Update Thread

Change status, override classification, set assignee, attach notes, or snooze.

Endpoint

PUT /v1/inbox/threads/{id}

Body Parameters

ParameterTypeRequiredDescription
statusstringNoopen, handled, or snoozed. Setting status to non-snoozed clears snoozed_until.
classification_overridestringNoOne of the seven classifications. Survives reclassification — the classifier worker won't overwrite manually-set labels.
assigned_to_user_idintegerNoOrg member ID.
notesstringNoFree-text notes attached to the thread.
snooze_hoursintegerNo1–720 (30 days). Combined with status=snoozed to set snoozed_until.

Example Request

curl -X PUT \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"status": "snoozed", "snooze_hours": 48, "notes": "Following up after their board meeting."}' \
"https://api.crondb.com/v1/inbox/threads/1284"

The response is the updated thread object in the same shape as the list endpoint items.


Bulk Thread Action

Apply mark-handled, reopen, snooze, assign, or classification override across many threads in one round-trip.

Endpoint

POST /v1/inbox/threads/bulk

Body Parameters

ParameterTypeRequiredDescription
thread_idsarray<integer>Yes1–500 thread IDs.
actionstringYesmark_handled, reopen, snooze, assign, or mark_classification.
snooze_hoursintegerIf action=snooze1–720.
assigned_to_user_idinteger|nullIf action=assignPass null to unassign.
classificationstringIf action=mark_classificationOne of the seven classifications. Stamps a manual override on the latest reply per thread.

Example Request

curl -X POST \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"thread_ids": [1284, 1287, 1291],
"action": "snooze",
"snooze_hours": 72
}' \
"https://api.crondb.com/v1/inbox/threads/bulk"

Response

{
"updated": 3,
"thread_ids": [1284, 1287, 1291]
}

Reclassify Threads

Force the classifier to re-run on every reply in the listed threads on its next poll. Useful after editing your classifier prompt or model. Manual overrides are preserved — replies whose latest classification was set via classification_override keep that label.

Endpoint

POST /v1/inbox/reclassify-bulk

Body Parameters

ParameterTypeRequiredDescription
thread_idsarray<integer>Yes1–500 thread IDs.

Response

{
"queued": 5,
"preserved_overrides": 1,
"thread_ids": [1284, 1287, 1291],
"next_run_within_seconds": 30
}
FieldDescription
queuedNumber of reply classifications cleared and now eligible for re-classification on the worker's next pass.
preserved_overridesManually-overridden classifications were kept untouched.
next_run_within_secondsUpper bound until the classifier worker picks up the queued replies.

Suggest Reply

Draft a reply to the latest inbound on a thread using your connected BYOK LLM. The draft is plain text — load it into your composer and edit before sending.

Endpoint

POST /v1/inbox/threads/{id}/suggest-reply

Body Parameters

ParameterTypeRequiredDescription
tonestringNofriendly, concise, professional, or enthusiastic. Defaults to a classification-appropriate tone (e.g. interested → enthusiastic, objection → empathetic).
extra_contextstringNoUp to 2000 chars of additional context the LLM should fold in (e.g. "we just shipped feature X").

The endpoint returns 400 for threads classified unsubscribe or bounce (consider marking the thread handled instead).

Rate Limit

10 calls per user per minute. The cap exists so a misclick or a runaway script can't burn through your BYOK budget.

Example Request

curl -X POST \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"tone": "concise", "extra_context": "Available next Tuesday after 2pm UTC."}' \
"https://api.crondb.com/v1/inbox/threads/1284/suggest-reply"

Response

{
"draft": "Happy to share — sending the deck over now. Could we lock in 30 minutes next Tuesday after 2pm UTC?",
"model": "deepseek-chat",
"provider": "deepseek",
"cost_estimate_usd": 0.00021,
"latency_ms": 842,
"based_on_classification": "interested",
"tone": "concise"
}

The cost is logged to the AI cost dashboard against action_type=inbox_suggest_reply.


Notes

  • The classifier worker runs in the background. Newly-arrived replies appear in the inbox within ~5 minutes.
  • Manual classification overrides survive reclassification — they're preserved on the manually_overridden_to field of the latest reply's classification row.
  • Snoozed threads automatically rejoin the open filter once their snooze window expires; no separate reaper needed.
  • suggest-reply requires at least one connected BYOK provider (OpenAI, Anthropic, DeepSeek, or Gemini). Connect keys at app.crondb.com/dashboard/ai-providers.

Next Steps

  • Sequences — Create the outreach sequences whose replies populate this inbox.
  • Send Accounts — Connect the mailboxes the classifier reads from.
  • Webhooks — Receive a webhook when a reply is classified interested.