Guide
Send your first email
Single sends, multi-inbox fan-out, scheduling, threading.
POST /v1/sends is the primitive. Whether you’re firing a one-off, blasting a sequence step across an audience, or replying to a thread, the same endpoint creates the same Send row, enqueues the same worker job, and fires the same events. Campaigns are not load-bearing — they’re one orchestration shape that produces sends, not the only way.
Single send
The simplest call: one inbox, one recipient, immediate delivery.
curl -X POST "https://api.clement.so/v1/sends" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{
"inbox_id": "inb_01HZ8X3J9K7P2QV6R5N4M3T2L1",
"to": [{"email": "alice@example.com", "name": "Alice"}],
"subject": "Quick question about your stack",
"body_text": "Hi {{first_name}}, would love to chat.",
"variables": {"first_name": "Alice"}
}'
import { Clement } from "@clement/sdk";
const c = new Clement({ apiKey: process.env.CLEMENT_API_KEY! });
const send = await c.sends.create(
{
inbox_id: "inb_01HZ8X3J9K7P2QV6R5N4M3T2L1",
to: [{ email: "alice@example.com", name: "Alice" }],
subject: "Quick question about your stack",
body_text: "Hi {{first_name}}, would love to chat.",
variables: { first_name: "Alice" },
},
{ idempotencyKey: crypto.randomUUID() },
);
A few rules:
Idempotency-Keyis required. Use a stable UUID per logical send — retries with the same key dedupe. A retry that arrives while the first request is still running gets 409idempotency_request_in_progress; retry again shortly. Errors aren’t cached, so after a 4xx/5xx you can fix the cause and retry with the same key.- At least one of
body_htmlorbody_textis required; the other is auto-derived. variablesare merged into{{mustache}}placeholders. Values are HTML-escaped automatically inbody_html.metadata(a{string: string}dict, max 20 keys × 256 chars) is echoed back on every event for that send — use it to correlate to your own records.append_signature: trueappends the sending inbox’s signature (set viaPATCH /v1/inboxes/{id}) to each body part you supply. It defaults tofalse: ad-hoc sends go out exactly as written. Campaign sends always carry the signature. The inbox’sreply_to, if set, applies to every send. Replies are only detected in inboxes connected to the workspace, so pointreply_toat one of them; replies to any other address don’t stop the sequence.
Schedule for later
Set send_at to an RFC-3339 timestamp:
{ "send_at": "2026-05-01T14:00:00Z", ... }
Scheduled sends still respect per-inbox daily caps and business-hours rules. To bypass those (admin scope only), pass respect_schedule: false.
Multi-inbox fan-out
To send the same email through multiple inboxes — round-robin or least-busy — use from_strategy:
{
"from_strategy": { "type": "rotate", "inbox_ids": ["inb_A", "inb_B", "inb_C"] },
"to": [
{ "email": "alice@example.com" },
{ "email": "bob@example.com" }
],
"subject": "...",
"body_text": "..."
}
from_strategy.type is one of:
single— requiresinbox_id(notinbox_ids).rotate— round-robin across the listed inboxes.least_busy— picks the inbox with the most remaining capacity in the current window.
Fan-out returns a BulkSendResult with one entry per recipient — the IDs of the created sends and any per-recipient errors (suppression, missing email, etc.). Caps and ramp-ups still apply.
Plan and monthly quota
Live sends need an active or trialing plan: without one, POST /v1/sends and thread replies return 402 subscription_required. A send queued with send_at is checked again when it goes out; if the plan has lapsed by then it fails with failure_reason: "subscription_required". Each recipient counts toward the plan’s monthly send quota as soon as the send is created (queued and in-flight sends included; sends canceled before pickup don’t count), and a fan-out is checked as a whole: if the batch doesn’t fit, nothing is sent and you get 429 quota_exceeded. Test mode is never gated.
Reply in a thread
curl -X POST "https://api.clement.so/v1/threads/thr_01HZ.../replies" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
--data '{ "body_text": "Following up on this — does Tuesday still work?" }'
POST /v1/threads/{id}/replies is sugar over POST /v1/sends that copies the thread’s headers and inbox. Identical engine underneath, including append_signature (default false).
You can also pass thread_id to POST /v1/sends directly. The send must come from the thread’s own inbox (inbox_id, or a single from_strategy); any other inbox returns 400, since a different mailbox can’t continue that conversation. The reply cites the thread’s latest message in In-Reply-To unless you set in_reply_to_message_id.
Cancel a scheduled send
Only valid while the send is in scheduled state:
curl -X POST "https://api.clement.so/v1/sends/snd_.../cancel" \
-H "Authorization: Bearer $CLEMENT_API_KEY"
Once the worker has the lock and state has flipped to sending, cancellation returns 409 — the email is already on its way out the door.
Lifecycle and events
Each send fires events:
send.scheduled— row created.send.sending— worker has the lock.send.sentorsend.failed— provider responded.- Subsequent events on the resulting message:
message.opened,message.clicked,message.replied,message.bounced.
All of them carry the original send’s metadata. Subscribe to whichever you care about via webhooks.