Skip to content

HTTP Events API

Pull your account’s delivery events over HTTP. This is the read counterpart to Webhooks: the same event vocabulary, fetched on your schedule instead of pushed to an endpoint.

GET https://smtp.atmos.email/v1/events

Identical to the Send API. Pass your DID and API key in the request headers:

X-Atmos-DID: did:plc:your-did-here
Authorization: Bearer atmos_your-api-key

You can also pass the DID as a ?did= query parameter. Every event returned is scoped to the authenticating DID. There is no parameter that widens the scope to another account.

Param Type Default Description
limit integer 50 Page size. Values above 200 are clamped to 200. Must be a positive integer.
cursor string none Opaque pagination token from a previous response’s nextCursor. Fetches the next (older) page.
type string none Filter to a single event type. One of message.delivered, message.bounced, message.complained, domain.verified. Omit to return all types.
since string none RFC3339 timestamp. Returns only events at or after this time.

The pull API covers message delivery events and domain verification. Webhooks additionally carry account, domain, effective-policy, rejection, and throttling events because those are low-latency state notifications rather than delivery-history rows.

Type Meaning
message.delivered A message you sent reached the recipient’s mail server.
message.bounced A message you sent was rejected by the recipient’s mail server.
message.complained A recipient marked one of your messages as spam (feedback loop report).
domain.verified A sending domain finished verification.

200 on success:

{
"events": [
{
"id": "evt_1042",
"type": "message.delivered",
"timestamp": "2026-06-08T17:04:11Z",
"domain": "yourdomain.com",
"recipientDomain": "gmail.com",
"smtpCode": 250
}
],
"nextCursor": "MjAyNi0wNi0wOFQxNzowNDoxMVp8MTA0Mg"
}
Field Type Description
id string Stable identifier for the event.
type string One of the event types above.
timestamp string RFC3339 time the event occurred.
domain string Your sending domain the event belongs to.
recipientDomain string The recipient’s domain, when the event carries one.
smtpCode integer The recipient server’s SMTP response code, on delivery and bounce events.

Events are returned newest first, ordered by timestamp then id.

Pagination is cursor based, not offset based, so a steady stream of new events never shifts a page boundary and causes you to miss or repeat a row.

  1. Make the first request with a limit and no cursor.
  2. If the response includes a non-null nextCursor, pass it back as ?cursor= to fetch the next page.
  3. When nextCursor is null, you have reached the end.

The cursor is opaque. Do not parse or construct it. A malformed cursor returns INVALID_REQUEST (400).

To poll for new events, record the timestamp of the newest event you have seen and pass it as since on the next poll. The filter includes that timestamp, so deduplicate events by id.

Errors return 4xx/5xx with the same shape as the Send API:

{
"error": "description of what went wrong",
"code": "ERROR_CODE"
}
Code HTTP Meaning
AUTH_REQUIRED 400 / 401 Missing DID (X-Atmos-DID header or ?did= query) or missing Bearer API key.
INVALID_REQUEST 400 Invalid DID format, unknown type, bad since, non-positive limit, or malformed cursor.
AUTH_FAILED 401 API key does not match any enrolled domain on this DID.
METHOD_NOT_ALLOWED 405 Use GET.
INTERNAL_ERROR 500 Server-side error. Retryable.
curl "https://smtp.atmos.email/v1/events?limit=50" \
-H "X-Atmos-DID: did:plc:your-did" \
-H "Authorization: Bearer atmos_your-api-key"

Filter to bounces since a point in time:

curl "https://smtp.atmos.email/v1/events?type=message.bounced&since=2026-06-01T00:00:00Z" \
-H "X-Atmos-DID: did:plc:your-did" \
-H "Authorization: Bearer atmos_your-api-key"
async function* allEvents(did, apiKey) {
let cursor = null;
do {
const url = new URL("https://smtp.atmos.email/v1/events");
url.searchParams.set("limit", "200");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: {
"X-Atmos-DID": did,
"Authorization": `Bearer ${apiKey}`,
},
});
const page = await res.json();
yield* page.events;
cursor = page.nextCursor;
} while (cursor);
}
import requests
def all_events(did, api_key):
cursor = None
while True:
params = {"limit": 200}
if cursor:
params["cursor"] = cursor
resp = requests.get(
"https://smtp.atmos.email/v1/events",
headers={
"X-Atmos-DID": did,
"Authorization": f"Bearer {api_key}",
},
params=params,
)
page = resp.json()
yield from page["events"]
cursor = page["nextCursor"]
if cursor is None:
break
  • The events vocabulary is the same as Webhooks. Use webhooks for low-latency push, the events API for backfill, reconciliation, or when running an endpoint is not practical.
  • A bounce event carries the recipient server’s smtpCode. Codes at 500 and above are permanent (hard) bounces; see Bounces and suppression.
  • The API returns at most 200 events per request. Walk the nextCursor to read more.
  • See Troubleshooting for common errors.

Back to docs