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.
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
| Header | Value |
|---|---|
| Authorization | Bearer cdb_your_api_key |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | open, handled, or snoozed. When open is requested, snoozed threads whose window has elapsed are auto-included. |
classification | string | No | interested, not_now, objection, ooo, unsubscribe, bounce, other. |
assigned_to | integer | No | Filter by assignee. Pass 0 for "unassigned" (NULL). |
domain | string | No | Restrict to threads from a single domain. |
limit | integer | No | Page size (default 50, max 200). |
offset | integer | No | Skip 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
| Field | Type | Description |
|---|---|---|
total | integer | Total threads matching the filters (ignores limit/offset). |
items[].id | integer | Internal primary key — pass to GET /threads/{id}. |
items[].thread_id | string | Stable hash key joining replies into the same thread across send accounts. |
items[].last_classification | string | Latest reply's classification. |
items[].last_classification_confidence | number | 0.0–1.0; 1.0 indicates a manual override. |
items[].status | string | open, handled, or snoozed. |
items[].assigned_to_user_id | integer|null | Org member assigned to handle this thread. |
items[].snoozed_until | string|null | When 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
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Same values as list. |
classification | string | No | Same values as list. |
assigned_to | integer | No | 0 = unassigned. |
domain | string | No | Limit 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Thread 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
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | open, handled, or snoozed. Setting status to non-snoozed clears snoozed_until. |
classification_override | string | No | One of the seven classifications. Survives reclassification — the classifier worker won't overwrite manually-set labels. |
assigned_to_user_id | integer | No | Org member ID. |
notes | string | No | Free-text notes attached to the thread. |
snooze_hours | integer | No | 1–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
| Parameter | Type | Required | Description |
|---|---|---|---|
thread_ids | array<integer> | Yes | 1–500 thread IDs. |
action | string | Yes | mark_handled, reopen, snooze, assign, or mark_classification. |
snooze_hours | integer | If action=snooze | 1–720. |
assigned_to_user_id | integer|null | If action=assign | Pass null to unassign. |
classification | string | If action=mark_classification | One 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
| Parameter | Type | Required | Description |
|---|---|---|---|
thread_ids | array<integer> | Yes | 1–500 thread IDs. |
Response
{
"queued": 5,
"preserved_overrides": 1,
"thread_ids": [1284, 1287, 1291],
"next_run_within_seconds": 30
}
| Field | Description |
|---|---|
queued | Number of reply classifications cleared and now eligible for re-classification on the worker's next pass. |
preserved_overrides | Manually-overridden classifications were kept untouched. |
next_run_within_seconds | Upper 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
| Parameter | Type | Required | Description |
|---|---|---|---|
tone | string | No | friendly, concise, professional, or enthusiastic. Defaults to a classification-appropriate tone (e.g. interested → enthusiastic, objection → empathetic). |
extra_context | string | No | Up 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_tofield of the latest reply's classification row. - Snoozed threads automatically rejoin the
openfilter once their snooze window expires; no separate reaper needed. suggest-replyrequires at least one connected BYOK provider (OpenAI, Anthropic, DeepSeek, or Gemini). Connect keys atapp.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.