Skip to main content
POST
/
v1
/
identities
/
{handle}
/
send
curl --request POST \
  --url https://api.inboxbase.ai/v1/identities/{handle}/send \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "to": "morgan@northwindrobotics.com",
  "subject": "Quick intro — fleet rotation",
  "text": "Hi Morgan, ...",
  "noReplyEventAfter": "4h"
}
'
{
  "identity": "<string>",
  "queued": 123,
  "rejected": 123,
  "results": [
    {
      "to": "jsmith@example.com",
      "pendingId": "<string>",
      "pinnedAccountId": "<string>",
      "dispatchAt": 123,
      "dispatchAtIso": "2023-11-07T05:31:56Z",
      "convId": "<string>"
    }
  ]
}
Every send is queued — we accept the request, classify the recipient, pin (or race) a backing mailbox, and the dispatcher delivers from there. The response carries a pendingId you can correlate with the downstream email.sent webhook (or email.send_failed_permanently on a hard fail). The body has two shapes — new conversation or reply — that are mutually exclusive. New conversations need to and subject; replies need convId and we resolve recipient, subject (with Re:), threading headers, and the previously-pinned mailbox from the existing thread. text and html can both be set; recipients see the multipart message their client prefers. Most production senders set both.

Send classification

The response includes sendClass, which tells you how we routed:
  • cold_first_contact — first message ever from this identity to this recipient. We pace these with the identity’s drip schedule and honor the working-hours window. The first available mailbox to claim the row owns the recipient from then on; subsequent sends use the same mailbox.
  • cold_followup — second-or-later cold message to a recipient who hasn’t replied in the last 3 outbounds. Same drip + working hours as a first contact, but pinned to a known mailbox.
  • warm — replying to a recipient who responded within their last 3 outbounds. Bypasses drip pacing and the working-hours window — responsive recipients get answered as fast as the dispatcher fires.
pinnedAccountId is the backing mailbox id (only meaningful for cold_followup and warm; null on cold_first_contact until a mailbox claims the row).

Labels

Label the conversation at queue time by passing labels: [...]. Idempotent on the conv (re-labeling an already-labeled conv is a no-op), so sequencer flows can pass the same campaign label on every step.
{
  "to": "morgan@northwindrobotics.com",
  "subject": "Quick intro",
  "text": "...",
  "labels": ["campaign:Q4-launch", "priority:hot"]
}
The per-label rollup analytics (sent / replied / positive / bounced) pivot off these labels — see GET /identities/:id/labels/stats. Free-form strings; we recommend a key:value convention so labels filter cleanly on prefix.

No-reply nudges

Pass noReplyEventAfter (duration string "1d" / "4h" / "30m" or a raw number of milliseconds) to schedule an email.no_reply webhook + event when no inbound lands on the conversation within that window. Default: 1 day. Floor: 60 seconds.

Idempotency

Pass Idempotency-Key to make retries safe. Same key with the same body returns the original response (header Idempotent-Replayed: true); same key with a different body returns 409. Keys live for 24 hours, scoped per-org. For sequencers the right key is <leadId>:<step> — if your worker crashes after we accepted the send but before your DB recorded it, the retry returns the same response and you don’t double-send.

Stitching to external threads

If the conversation started outside 12m (an imported CRM history, forwarded mail), pass inReplyTo and references on the new-conv shape. We thread the new send onto the external chain.
{
  "to":         "morgan@northwindrobotics.com",
  "subject":    "Re: Fleet rotation",
  "text":       "Following up...",
  "inReplyTo":  "<original-message-id@northwindrobotics.com>",
  "references": ["<original-message-id@northwindrobotics.com>"]
}

Status codes

  • 202 Accepted — at least one row was queued. The body’s results[] array tells you per-recipient outcome.
  • 429 Too Many Requests — every recipient on the request was rejected (do_not_contact, no_eligible_pool, inactive, suspended). Inspect results[i].reason to discriminate.
  • 409 ConflictIdempotency-Key reused with a different request body.

What we don’t support yet

  • Scheduled sends. No sendAt. We pace cold sends via the identity scheduling config (working hours, drip interval); for one-shot future sends, run a scheduler in your own infra and call at fire time.
  • Attachments. Not supported.
  • Custom headers. No way to set List-Unsubscribe / List-Unsubscribe-Post today. High-priority because of bulk-sender requirements; we’ll ship it.

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Headers

Idempotency-Key
string

Up to 255 characters. Same key with same body returns the original response (with Idempotent-Replayed: true); same key with a different body returns 409. Keys live 24 hours, scoped per-org.

Maximum string length: 255

Path Parameters

handle
string
required

Identity handle, URL-encoded.

Example:

"alice.acme@inboxbase.ai"

Body

application/json
to
required

Recipient address. Pass an array to send to multiple recipients; each becomes its own conversation.

subject
string
required

Subject line.

Minimum string length: 1
text
string

Plain-text body. At least one of text / html is required.

html
string

HTML body.

inReplyTo
string

Message-ID of an external message you're stitching to.

references
string[]

Cumulative References chain for stitching to an external thread.

noReplyEventAfter
default:1d

A duration string ("4h", "3 days", "90 minutes", "1.5h") or a raw number interpreted as milliseconds. Floor 1 minute.

Example:

"4h"

labels
string[]

Conversation tags applied at queue time (e.g. ["campaign:Q4-launch", "priority:hot"]). Idempotent — a tag already on the conv is a no-op. Pivot label-rollup analytics off these via /identities/:id/labels/stats.

Maximum array length: 24
Required string length: 1 - 120

Response

At least one recipient accepted.

Queue-first send response. The dispatcher delivers each row asynchronously; correlate pendingId with the downstream email.sent webhook to know when delivery actually fired.

status
enum<string>
required

queued if at least one recipient queued; rejected if the whole batch was rejected.

Available options:
queued,
rejected
identity
string
required

Identity handle echoed back.

queued
integer
required

Count of recipients successfully queued.

rejected
integer
required

Count of recipients rejected.

results
object[]
required