Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Scheduling and Batches

Two related features for coordinating multi-message sends: scheduled delivery (send later) and batches (group many messages under a single ID for aggregate reporting and collective cancellation).

Scheduled delivery#

Set send_at to a future Unix timestamp on /v3/mail/send:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
curl -X POST https://scaisend.scailabs.ai/v3/mail/send \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [{"to": [{"email": "ada@example.com"}]}],
    "from": {"email": "hello@mail.example.com"},
    "subject": "Weekly digest",
    "content": [{"type": "text/plain", "value": "..."}],
    "send_at": 1714060800
  }'
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import os, time, httpx

httpx.post(
    "https://scaisend.scailabs.ai/v3/mail/send",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_API_KEY']}"},
    json={
        "personalizations": [{"to": [{"email": "ada@example.com"}]}],
        "from": {"email": "hello@mail.example.com"},
        "subject": "Weekly digest",
        "content": [{"type": "text/plain", "value": "..."}],
        "send_at": int(time.time()) + 86400,  # 24 hours from now
    },
)
typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
await fetch("https://scaisend.scailabs.ai/v3/mail/send", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAISEND_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    personalizations: [{ to: [{ email: "ada@example.com" }] }],
    from: { email: "hello@mail.example.com" },
    subject: "Weekly digest",
    content: [{ type: "text/plain", value: "..." }],
    send_at: Math.floor(Date.now() / 1000) + 86400,
  }),
});

Behavior:

  • The message is accepted immediately (202). A message_id is returned.
  • The message sits in status QUEUED until send_at arrives.
  • At send_at, the message enters the normal render → DKIM → SMTP pipeline.
  • Requires the mail.schedule scope. A key without it gets 403.

Granularity: seconds. send_at values in the past (or within a few seconds of now) are treated as "send now."

Per-personalization scheduling is supported — you can schedule different personalizations at different times using send_at inside each personalization block. Top-level send_at applies to any personalization that doesn't override.

Cancelling a scheduled send#

Before the message leaves QUEUED:

bash
1
2
curl -X POST https://scaisend.scailabs.ai/v3/messages/msg_01HXYZ/cancel \
  -H "Authorization: Bearer $SCAISEND_API_KEY"

Requires the mail.cancel scope. The message transitions to status CANCELLED and is not sent.

If the message has already entered PROCESSING (the worker has started rendering it), cancellation may or may not succeed — it races with the send pipeline. The response will tell you:

json
1
{"message_id": "msg_01HXYZ", "status": "cancelled", "message": "Cancelled before delivery"}

or:

json
1
{"message_id": "msg_01HXYZ", "status": "sending", "message": "Already in flight; cancellation ignored"}

Batches#

A batch is a group of messages tagged with a shared batch_id. Batches let you:

  • Query aggregate status across many messages with one request.
  • Cancel an in-flight batch by cancelling each message (the API doesn't batch-cancel, but you can iterate by batch_id).

Create a batch ID#

bash
1
2
curl -X POST https://scaisend.scailabs.ai/v3/mail/batch \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
python
1
2
3
4
5
resp = httpx.post(
    "https://scaisend.scailabs.ai/v3/mail/batch",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_API_KEY']}"},
)
batch_id = resp.json()["batch_id"]
typescript
1
2
3
4
5
const resp = await fetch("https://scaisend.scailabs.ai/v3/mail/batch", {
  method: "POST",
  headers: { "Authorization": `Bearer ${process.env.SCAISEND_API_KEY}` },
});
const { batch_id } = await resp.json();

Response:

json
1
{"batch_id": "bat_01HXYZ"}

Include on sends#

Every subsequent /v3/mail/send that belongs in the batch includes the batch ID:

json
1
2
3
4
5
6
7
{
  "personalizations": [{"to": [{"email": "ada@example.com"}]}],
  "from": {"email": "hello@mail.example.com"},
  "subject": "Campaign message",
  "content": [{"type": "text/plain", "value": "..."}],
  "batch_id": "bat_01HXYZ"
}

The batch_id is stored on each message. Multiple tenants cannot share a batch — batch_ids are tenant-scoped.

Aggregate status#

bash
1
2
curl https://scaisend.scailabs.ai/v3/mail/batch/bat_01HXYZ \
  -H "Authorization: Bearer $SCAISEND_API_KEY"

Response:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "batch_id": "bat_01HXYZ",
  "total_messages": 4872,
  "status_counts": {
    "queued": 12,
    "processing": 8,
    "sent": 3200,
    "delivered": 1600,
    "bounced": 50,
    "failed": 2,
    "cancelled": 0,
    "sandbox": 0
  },
  "created_at": "2026-04-23T09:00:00Z",
  "completed_at": null,
  "is_complete": false
}

is_complete flips to true when every message has reached a terminal state (delivered, bounced, failed, cancelled, or sandbox).

Listing messages in a batch#

Batch status is an aggregate view. To see individual messages:

bash
1
2
curl "https://scaisend.scailabs.ai/v3/messages?batch_id=bat_01HXYZ&page_size=100" \
  -H "Authorization: Bearer $SCAISEND_API_KEY"

Paginate through the full list. Use status to filter — e.g., ?batch_id=bat_01HXYZ&status=bounced to see only bounced messages.

Cancelling a batch#

There's no single "cancel batch" endpoint. Cancel each message in the batch that's still QUEUED or PROCESSING:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os, httpx

api_key = os.environ["SCAISEND_API_KEY"]
base = "https://scaisend.scailabs.ai"
headers = {"Authorization": f"Bearer {api_key}"}

# List still-cancellable messages in the batch
page = 1
cancelled = 0
while True:
    resp = httpx.get(
        f"{base}/v3/messages",
        headers=headers,
        params={"batch_id": "bat_01HXYZ", "status": "queued", "page": page, "page_size": 100},
    )
    messages = resp.json()["messages"]
    if not messages:
        break
    for msg in messages:
        cancel = httpx.post(f"{base}/v3/messages/{msg['id']}/cancel", headers=headers)
        if cancel.status_code == 200:
            cancelled += 1
    if not resp.json()["messages"]:
        break
    page += 1
print(f"Cancelled {cancelled} messages.")

Messages already in flight (SENDING) can't be cancelled. This is physics: the SMTP conversation has started.

Common patterns#

Send a daily digest to all subscribers#

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import os, time, httpx

api_key = os.environ["SCAISEND_API_KEY"]
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}

# 1. Create a batch
batch_id = httpx.post(
    "https://scaisend.scailabs.ai/v3/mail/batch", headers=headers
).json()["batch_id"]

# 2. Send to each subscriber; all tagged with batch_id
for user in subscribers:
    httpx.post(
        "https://scaisend.scailabs.ai/v3/mail/send",
        headers=headers,
        json={
            "personalizations": [
                {
                    "to": [{"email": user.email}],
                    "dynamic_template_data": {"name": user.name, "digest": user.digest_data},
                }
            ],
            "from": {"email": "digest@mail.example.com"},
            "template_id": "d-daily-digest",
            "batch_id": batch_id,
            "categories": ["digest", "daily"],
        },
    )

# 3. Check status
resp = httpx.get(
    f"https://scaisend.scailabs.ai/v3/mail/batch/{batch_id}", headers=headers
)
print(resp.json()["status_counts"])

Schedule a time-zone-respecting marketing send#

Iterate users, calculate each user's 10 AM local time, and send each with a per-personalization send_at:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os, httpx
from datetime import datetime
import zoneinfo

api_key = os.environ["SCAISEND_API_KEY"]

for user in subscribers:
    local = zoneinfo.ZoneInfo(user.timezone)
    next_ten_am = datetime.now(local).replace(hour=10, minute=0, second=0, microsecond=0)
    send_at = int(next_ten_am.timestamp())

    httpx.post(
        "https://scaisend.scailabs.ai/v3/mail/send",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        json={
            "personalizations": [{"to": [{"email": user.email}]}],
            "from": {"email": "hello@mail.example.com"},
            "template_id": "d-promo",
            "send_at": send_at,
        },
    )

Every recipient gets the message at 10 AM their time. All scheduled at once; delivery staggered by ScaiSend's scheduler.

What's next#

Updated 2026-05-17 01:33:26 View source (.md) rev 1