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

Messages and Events

Once a message is accepted by /v3/mail/send, every delivery action against it — rendering, queueing, sending, bouncing, being opened — is recorded as an event on the message timeline. This page covers querying messages, reading their event history, and the management endpoints (cancel, retry, fail).

Base path: /v3/messages/ Auth: API key or JWT; mail.send (for retry/cancel/fail) or stats.read for plain reads — check your deployment.

The message lifecycle#

componentpascal
1
2
3
4
5
QUEUED  PROCESSING  RENDERED  SENDING  SENT  DELIVERED
                                    
                                    ├─► BOUNCED
                                    └─► FAILED
(alternate terminal states: CANCELLED, SANDBOX)
Status Meaning
QUEUED Accepted, waiting for the worker to pick up
PROCESSING Worker is rendering the template and building MIME
RENDERED MIME is built, waiting for SMTP service
SENDING SMTP service is actively connecting to recipient MX
SENT Accepted by recipient MX (awaiting async bounce confirmation if any)
DELIVERED Confirmed delivered (no bounce received, or 250 OK captured)
BOUNCED Permanent failure — either 5xx at send time or an async DSN
FAILED Non-delivery error (template render failure, unverified domain, rate-limited at SMTP layer after retries)
CANCELLED User cancelled before sending
SANDBOX Test send; validated but not delivered

Listing messages#

bash
1
2
curl "https://scaisend.scailabs.ai/v3/messages?page=1&page_size=25" \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import os, httpx

resp = httpx.get(
    "https://scaisend.scailabs.ai/v3/messages",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_API_KEY']}"},
    params={"page": 1, "page_size": 25, "status": "delivered"},
)
data = resp.json()
for msg in data["messages"]:
    print(msg["id"], msg["status"], msg["subject"], msg["to_emails"])
typescript
1
2
3
4
5
const params = new URLSearchParams({ page: "1", page_size: "25", status: "delivered" });
const resp = await fetch(`https://scaisend.scailabs.ai/v3/messages?${params}`, {
  headers: { "Authorization": `Bearer ${process.env.SCAISEND_API_KEY}` },
});
const data = await resp.json();

Filters#

Parameter Notes
status Filter by lifecycle status (queued, delivered, bounced, etc.)
to_email Exact match on a recipient address
from_email Exact match on the sender address
subject Substring search on subject
batch_id Messages belonging to a batch
template_id Messages sent using a template
category Messages tagged with this category
start_date / end_date ISO-8601 or YYYY-MM-DD
page 1-indexed page number (default 1)
page_size 1–100 (default 25)

Response#

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "messages": [
    {
      "id": "msg_01HXYZ",
      "status": "delivered",
      "from_email": "hello@mail.example.com",
      "from_name": "Acme",
      "subject": "Welcome to Acme",
      "to_emails": ["ada@example.com"],
      "template_id": "d-welcome",
      "batch_id": null,
      "categories": ["onboarding"],
      "created_at": "2026-04-23T10:00:00Z",
      "delivered_at": "2026-04-23T10:00:02Z"
    }
  ],
  "total": 4872,
  "page": 1,
  "page_size": 25,
  "total_pages": 195
}

The list view is a summary. To see the body, headers, and event timeline, get the individual message.

Getting a single message#

bash
1
2
curl https://scaisend.scailabs.ai/v3/messages/msg_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
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
  "id": "msg_01HXYZ",
  "status": "delivered",
  "from_email": "hello@mail.example.com",
  "from_name": "Acme",
  "subject": "Welcome to Acme",
  "to_emails": ["ada@example.com"],
  "cc_emails": [],
  "bcc_emails": [],
  "reply_to": "support@example.com",
  "categories": ["onboarding"],
  "template_id": "d-welcome",
  "batch_id": null,
  "custom_args": {"user_id": "u_12345"},
  "custom_headers": {},
  "tracking_settings": {
    "open_tracking": true,
    "click_tracking": true,
    "subscription_tracking": false
  },
  "retry_count": 0,
  "next_retry_at": null,
  "html_content": "<html>...</html>",
  "plain_content": "...",
  "created_at": "2026-04-23T10:00:00Z",
  "queued_at": "2026-04-23T10:00:00Z",
  "sent_at": "2026-04-23T10:00:01Z",
  "delivered_at": "2026-04-23T10:00:02Z",
  "error_message": null,
  "events": [
    {
      "event_type": "processed",
      "recipient_email": "ada@example.com",
      "timestamp": "2026-04-23T10:00:00Z"
    },
    {
      "event_type": "delivered",
      "recipient_email": "ada@example.com",
      "timestamp": "2026-04-23T10:00:02Z",
      "smtp_response": "250 2.0.0 OK",
      "metadata": {"mx_host": "mx1.example.com", "tls_version": "TLSv1.3"}
    },
    {
      "event_type": "open",
      "recipient_email": "ada@example.com",
      "timestamp": "2026-04-23T10:15:30Z",
      "user_agent": "Mozilla/5.0 ...",
      "ip_address": "203.0.113.42"
    }
  ]
}

The events[] array is the full message timeline in chronological order. Every event ScaiSend has recorded for this message lands here — it's the same events fanned out to your webhooks, just pull-based.

Event fields#

Events on the timeline include common fields plus type-specific extras:

Field Always present? Notes
event_type Yes processed, deferred, delivered, bounce, blocked, dropped, open, click, spam_report, unsubscribe, group_unsubscribe, group_resubscribe
recipient_email Yes The recipient this event pertains to
timestamp Yes ISO-8601 UTC
url On click The original URL that was clicked
user_agent On open, click The recipient's client identifier
ip_address On open, click The recipient's IP at time of interaction
bounce_type On bounce hard, soft, block
bounce_reason On bounce Human-readable reason string
smtp_response On delivered, bounce, deferred, blocked Raw SMTP response line
metadata Sometimes Arbitrary extra fields

See Events and Webhooks for the full metadata reference per event type.

Message management#

Three actions on a single message: cancel, retry, and fail.

Cancel#

Stops a scheduled or in-progress message from being sent. Works on QUEUED and PROCESSING. Fails (by design) if the message is already in SENDING or later.

bash
1
2
curl -X POST https://scaisend.scailabs.ai/v3/messages/msg_01HXYZ/cancel \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
json
1
{"message_id": "msg_01HXYZ", "status": "cancelled", "message": "Cancelled before delivery"}

Requires mail.cancel scope.

Retry#

Force-requeues a message that's stuck or in a terminal failure state. Works on PROCESSING, SENDING, FAILED, and BOUNCED.

bash
1
2
curl -X POST https://scaisend.scailabs.ai/v3/messages/msg_01HXYZ/retry \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
json
1
{"message_id": "msg_01HXYZ", "status": "queued", "message": "Re-queued for delivery"}

Use cases:

  • A soft bounce that you think is resolvable. The recipient's mailbox was full at the time; you've waited; retry.
  • A stuck message. Rare, but if a message sat in PROCESSING too long (worker crashed mid-render), retry kicks it back into the queue.
  • A post-mortem redelivery. Bounced because of a policy issue that's since been fixed (SPF was wrong; you fixed SPF; retry).

Retrying a hard bounce (bounce_type: hard) does not remove the address from the suppression list — you'll just get another bounce event, unless you also manually remove the suppression first.

Fail#

Mark a stuck message as FAILED manually. Used to clean up orphans:

bash
1
2
3
4
curl -X POST https://scaisend.scailabs.ai/v3/messages/msg_01HXYZ/fail \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Stuck in processing for 2 hours; marking failed"}'

The reason is optional; it's recorded as the message's error_message. The message transitions to FAILED and no further delivery is attempted.

Prefer retry over fail when the state is ambiguous. Use fail only when you're certain the message should not be redelivered.

Performance notes#

  • GET /v3/messages/{id} is cheap (< 10 ms typically) — the message, events, and body come from a single query.
  • GET /v3/messages is indexed on tenant_id, created_at, status, and common filter fields. Filters on high-cardinality fields (to_email, subject) fall back to full table scans on very large datasets; if you need those often, add a downstream index or an Elasticsearch export.
  • Pagination uses offset/limit under the hood. Deep pagination (page > ~100) gets slow on large tenants; prefer filtering by date range to narrow.

What's next#

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