Scheduling
Defer a send up to 30 days, move it, or cancel it — and what stays mutable until the moment it goes out.
Pass scheduled_at on a send or a reply and the message is held until then. The API returns it immediately with status: "scheduled", and it stays yours to move or cancel right up until it leaves.
const email = await aiinbx.emails.send({
from: "Ada <ada@example.com>",
to: "grace@example.com",
subject: "Monday reminder",
text: "The review is at 10.",
scheduled_at: "2026-09-14T09:00:00Z",
})
console.log(email.status) // "scheduled"email = client.emails.send(
{
"from_": "Ada <ada@example.com>",
"to": ["grace@example.com"],
"subject": "Monday reminder",
"text": "The review is at 10.",
"scheduled_at": "2026-09-14T09:00:00Z",
}
)curl https://api.aiinbx.com/api/v2/emails \
-H "Authorization: Bearer $AI_INBX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Ada <ada@example.com>",
"to": "grace@example.com",
"subject": "Monday reminder",
"text": "The review is at 10.",
"scheduled_at": "2026-09-14T09:00:00Z"
}'scheduled_at is an RFC 3339 timestamp with an offset (2026-09-14T09:00:00Z or 2026-09-14T11:00:00+02:00), at most 30 days out. A naive timestamp is rejected with invalid_request — the offset is required precisely because “9am” without one means six different instants.
Rescheduling
PATCH /emails/{id} moves a scheduled message. The same 30-day bound applies from the moment of the call.
const moved = await aiinbx.emails.reschedule("eml_...", {
scheduled_at: "2026-09-15T09:00:00Z",
})moved = client.emails.reschedule("eml_...", scheduled_at="2026-09-15T09:00:00Z")curl -X PATCH https://api.aiinbx.com/api/v2/emails/eml_... \
-H "Authorization: Bearer $AI_INBX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "scheduled_at": "2026-09-15T09:00:00Z" }'Only a message still in scheduled status can move. Once it has begun sending, the call fails with 409 not_scheduled.
Canceling
const canceled = await aiinbx.emails.cancel("eml_...")
console.log(canceled.status) // "canceled"canceled = client.emails.cancel("eml_...")curl -X POST https://api.aiinbx.com/api/v2/emails/eml_.../cancel \
-H "Authorization: Bearer $AI_INBX_API_KEY"Cancellation is terminal — a canceled message can’t be rescheduled back into flight. Send a new one instead.
Both rescheduling and cancellation need a key with the sending or full scope.
Finding what’s scheduled
status: "scheduled" on the email list is the queue of everything still pending:
for await (const email of aiinbx.emails.list({ status: "scheduled" })) {
console.log(email.scheduled_at, email.subject, email.to)
}for email in client.emails.iter(status="scheduled"):
print(email["scheduled_at"], email["subject"], email["to"])curl "https://api.aiinbx.com/api/v2/emails?status=scheduled&limit=100" \
-H "Authorization: Bearer $AI_INBX_API_KEY"Composed at send time, not at schedule time
A scheduled message is stored as an intent and composed at the instant it goes out. Two consequences worth designing around:
- Suppressions are applied then, not now. A recipient who unsubscribes between scheduling and sending is dropped, and shows up in the delivery record rather than in the response you already received.
- Pacing rules are evaluated then, too. A message scheduled for 3am under an hours rule that forbids 3am is held until the window opens;
scheduled_atsets the earliest time it may go, not a guarantee of the exact instant.
If you need a message to ignore the pacing rules entirely, set pacing.skip on it:
{
"scheduled_at": "2026-09-14T09:00:00Z",
"pacing": { "skip": true }
}
Scheduling versus pacing
They solve different problems and compose fine:
| Scheduling | Pacing | |
|---|---|---|
| Set on | One message | Workspace-wide rules |
| Answers | “Not before this instant” | “Not faster than this, and not outside these hours” |
| Changed by | reschedule / cancel |
Rule edits, or releasing from the queue |
Use scheduled_at for “send this Monday morning”. Use pacing for “never more than 200 an hour from this domain”.