# SendOps Developer Documentation > Developer documentation and REST API reference for SendOps — the observability and management layer for Amazon SES. Covers authentication, rate limiting, pagination, errors, and every API endpoint with request/response examples. Each documentation page is also available as plain markdown by appending `.md` to its URL (e.g. `/api-reference/quickstart.md`). A concatenated corpus is available at `/llms-full.txt`. ## API Reference - [Overview](https://developers.sendops.dev/api-reference): What the SendOps Public API is, what it isn't, and how it fits alongside your existing AWS SES setup. - [Quickstart](https://developers.sendops.dev/api-reference/quickstart): Five-minute path from creating an API key to your first successful request. - [Authentication](https://developers.sendops.dev/api-reference/authentication): API key format, the Bearer header, environments, and the create/rotate/revoke lifecycle. - [Rate Limiting](https://developers.sendops.dev/api-reference/rate-limiting): Per-org request quotas, the headers we return on every response, and how to handle 429s. - [Errors](https://developers.sendops.dev/api-reference/errors): Every error the SendOps API returns is an RFC 7807 problem document. This page catalogues the codes and recommended client behaviour. - [Pagination](https://developers.sendops.dev/api-reference/pagination): Cursor-based pagination, the Link header, why we don't expose offsets or totals, and how to iterate safely. ## Meta - [Service liveness probe](https://developers.sendops.dev/api-reference/endpoints/meta/get-health): Unauthenticated liveness probe for uptime monitors and status pages. Returns a static `{status: "ok", commit, time}` payload — a 200 means the API is serving requests, not that every dependency is healthy. For dependency-aware health, watch the platform status page instead. The `commit` field is the deployed build identifier, useful for confirming which version of the API is responding. - [Scopes attached to the calling key](https://developers.sendops.dev/api-reference/endpoints/meta/get-scopes): Returns the scopes attached to the calling API key plus the key's identity (UUID, prefix, and environment `live`/`test`). Use this to verify a key authenticates correctly before issuing real traffic, and to confirm a recent scope change has taken effect. Any valid key may call this endpoint — no specific scope is required. Scope changes can take up to a few minutes to be reflected here. ## Messages - [List messages with cursor pagination](https://developers.sendops.dev/api-reference/endpoints/messages/list-messages): Returns one cursor-paginated page of message summaries, aggregated to one row per message. Results are ordered newest-first by `sent_at`. When `from` and `to` are omitted the window defaults to the trailing 30 days; a `from` older than your plan's retention is rejected with `403 plan_retention_exceeded` (the response carries a `retention_days` extension), it is not silently clamped. Use this for dashboards, reconciliation, and incremental export — page forward with the `next_cursor` from the response (or follow the `Link: <…>; rel="next"` header) until `has_more` is false. Filters compose with AND. The `recipient` filter is PII-gated: passing it without the `analytics.search.recipients` scope returns `403 invalid_scope`, not a silently-dropped filter. - [Get one message](https://developers.sendops.dev/api-reference/endpoints/messages/get-message): Returns the aggregated detail for a single message (the same `V1Message` projection as the list endpoint). Lookup is by the SES-issued message id, not an internal UUID. Cross-org lookups and unknown ids both return `404 not_found` — no information leak about whether a message exists in another org. Detail lookups go further back than your plan's analytics-retention window: up to 365 days of message detail is kept regardless of plan tier. Very old ids will eventually 404 once they fall outside that window. - [List the SES event timeline for one message](https://developers.sendops.dev/api-reference/endpoints/messages/get-message-events): Returns the full SES event timeline for one message (`Send` → `Delivery`/`Bounce`/`Reject`/`DeliveryDelay`, then `Open` and `Click` events as recipients interact). Events are sorted ascending by `occurred_at`. The response is not paginated — a typical message has fewer than a dozen events. Each event carries type-specific `metadata` (omitted on `Send`): | Type | metadata keys | |------------------|---------------| | `Bounce` | `bounce_type`, `bounce_sub_type`, `smtp_response` | | `Complaint` | `complaint_feedback_type` | | `Delivery` | `smtp_response`, `processing_time_ms` | | `DeliveryDelay` | `delay_type` | | `Reject` | `reject_reason` | | `Open` | `ip_address`, `user_agent` | | `Click` | `click_url`, `ip_address`, `user_agent` | A message with zero stored events — either unknown to the platform or belonging to another org — returns `404 not_found`. Same retention caveat as `getMessage`: events are kept for up to 365 days. - [List messages sent to one recipient (PII-gated)](https://developers.sendops.dev/api-reference/endpoints/messages/list-messages-by-recipient): Returns one cursor-paginated page of messages addressed to a specific recipient — the same shape as `listMessages`, but with the recipient pinned from the URL path. This route is gated solely on the `analytics.search.recipients` scope (the broader `messages.view` is not required) so you can grant audit-style recipient lookup without exposing the full message index. Match is case-sensitive on the stored recipient address. URL-encode `@` as `%40` (e.g. `user%40example.com`). Unknown recipients return `200` with `data: []` — never `404`. Same 30-day default window and plan-retention behavior as `listMessages`. ## Reports - [Deliverability time series](https://developers.sendops.dev/api-reference/endpoints/reports/get-deliverability-report): Time-bucketed counts of `sent`, `delivered`, `bounced`, `complained`, and `suppressed`, with a per-bucket `delivery_rate`. Numbers match what you see in the SendOps dashboard. Buckets are aligned to UTC (day = midnight UTC; week = Monday 00:00 UTC; month = first of month 00:00 UTC). Empty periods are **not zero-filled** — buckets with no underlying events are omitted entirely. Formula: `delivery_rate = delivered / sent`, a ratio in `[0, 1]`. When `sent` is zero, `delivery_rate` is `0.0` (never `null`). `suppressed` counts SES `Reject` events — sends SES dropped against the account-level suppression list — not the size of your suppression table. Default window is the trailing 30 days. A `from` older than your plan's retention returns `403 plan_retention_exceeded` with a `retention_days` extension. - [Engagement time series](https://developers.sendops.dev/api-reference/endpoints/reports/get-engagement-report): Time-bucketed open and click activity. Each bucket emits raw `sent`, `delivered`, total `opens`/`clicks`, and `unique_opens`/`unique_clicks` (distinct recipients who acted within the bucket). Rate formulas: - `open_rate = unique_opens / delivered` - `click_rate = unique_clicks / delivered` Both denominators are `delivered`, not `sent`, and the numerators are **unique** counts. Ratios are in `[0, 1]`; zero `delivered` yields `0.0`. Buckets align to UTC the same way as deliverability and empty buckets are not zero-filled. Caveat: for `week` and `month` buckets, a recipient who engages on more than one day inside the bucket may be counted more than once in `unique_opens`/`unique_clicks`. This matches the SendOps dashboard. Default window is the trailing 30 days; plan retention applies. - [Per-template engagement rollup](https://developers.sendops.dev/api-reference/endpoints/reports/get-template-performance-report): One row per template aggregating activity across the entire query window — no time bucketing. Use this to spot templates that bounce more, complain more, or under-engage compared to peers. Rows are sorted by `sent` descending, tie-broken by `template` ascending. Sends that aren't tied to a named template (raw `SendEmail` calls without a `Template` parameter) are excluded so they don't dominate the ranking. Rate formulas (note the **asymmetric** denominators, matching the dashboard): - `bounce_rate = bounced / sent` - `complaint_rate = complained / sent` - `open_rate = unique_opens / delivered` - `click_rate = unique_clicks / delivered` All ratios are in `[0, 1]`. Zero denominators yield `0.0`. The `template` field is the SES template name (the slug — the value SES received in the `Template` parameter), not a UI display name. Default window is the trailing 30 days; plan retention applies. No pagination — the response holds every template the org used in the window. ## Suppressions - [List suppressed recipients](https://developers.sendops.dev/api-reference/endpoints/suppressions/list-suppressions): Returns the calling org's AWS SES account-level suppression list. Results are read from SES through a short server-side cache (up to a minute), so newly-added suppressions may take that long to appear. SES populates this list automatically when bounce/complaint feedback arrives; the API is read-only — this endpoint does not add or remove entries. The `source` field is derived from `reason`: entries with reason `bounce` or `complaint` are reported as `source: ses_event` (the platform's event pipeline created them), and everything else is reported as `source: manual` (added directly via the AWS console, CLI, or admin tooling). It is not based on stored provenance — SES does not carry that metadata. Sorting is `suppressed_at` descending. When the `email` filter is set the endpoint switches to full-scan mode: it returns every match in a single response and ignores `limit`/`cursor`/`has_more`. Cursors are opaque base64 — stale or malformed cursors fall back to page 1 silently instead of erroring. - [Look up one suppressed recipient](https://developers.sendops.dev/api-reference/endpoints/suppressions/get-suppression): Returns the suppression record for an exact email address, or `404 not_found` when the address is not on your SES suppression list. Match is case-insensitive, but the returned `email` preserves SES's stored casing. Newly-added suppressions can take up to a minute to appear here. Percent-encode `@` as `%40` (e.g. `user%40example.com`). ## Undeliverable - [List undeliverable recipients (permanent failures + exclusions)](https://developers.sendops.dev/api-reference/endpoints/undeliverable/list-undeliverable): Returns recipients with permanent-failure events (Permanent bounces, Complaints, or Rejects) in your retention window — plus operator-cleared addresses as tombstones (`status: excluded`) so polling callers can keep their own suppression list in sync. ### Classification rules (stable across v1) A recipient appears on the list with `status: "listed"` when SendOps has ingested any of the following events for that address in the org's retention window AND no operator exclusion with `excluded_at` greater than the address's `last_seen_at` is in effect: | Event | Condition | Reason | | --------- | -------------------------------------- | ------------------ | | Bounce | `bounceType = Permanent` (any subtype) | `permanent_bounce` | | Complaint | any | `complaint` | | Reject | any | `rejected` | Transient bounces, DeliveryDelay, Send/Delivery/Open/Click events never qualify. Refining this classification is a v2 break — callers may write their suppression logic against the rules above and rely on them. ### Polling with `?since=` Callers maintaining their own suppression list should poll with `?since=`, anchored on the largest `last_changed_at` from their most-recent processed batch. The endpoint returns every row whose `last_changed_at >= since`, including: - newly-listed addresses (recent permanent failures) - re-listed addresses (operator-cleared but bounced again) - excluded tombstones (operator decided to allow this address) Apply rows by `status`: add `listed` rows to your local suppression list, remove `excluded` rows from it. The since boundary is inclusive on the second so a one-row overlap is possible — operations are idempotent so this is safe. ### Snapshot vs sync default With no `since`, the response defaults to `status: "listed"` only — a clean current-state snapshot. Pass `?since=...` to receive both listed and excluded rows. The discrimination is keyed off whether `since` is set; explicit `?status=` overrides the default. ### Retention caveat The listed portion is bounded by your plan's retention window. Exclusion tombstones live in durable storage and surface regardless of how long ago the address last bounced — a caller skipping a poll window will still see the tombstone when they next call with `?since=...`. Cursors are opaque base64 — stale or malformed cursors fall back to page 1 silently instead of erroring. The cursor paginates one snapshot; `?since=` is the real polling mechanism. - [Read the active classification rules for the calling org](https://developers.sendops.dev/api-reference/endpoints/undeliverable/get-undeliverable-rules): Returns the active rule set the /v1/undeliverable surface applies when classifying recipients (SND-713). Three rules are always-on (locked, cannot be disabled): permanent_bounce, complaint, rejected. Three rules are configurable per org, each with an `events` threshold (N) and `window_days` (M) — an address matches when ≥ N qualifying events have occurred in a rolling M-day window. When an address matches multiple rules, the wire `reason` is the highest-priority match in this order: permanent_bounce > complaint > rejected > repeated_transient > undetermined > soft_bounce_accumulation. Rules are read-only over the public API in v1 — changes are made through the dashboard (audit-logged, granted to owners + admins only). Callers should pin against `version` and re-fetch when the `X-SendOps-Rules-Version` header on /v1/undeliverable changes. ## Account - [Org / AWS / SES / plan snapshot](https://developers.sendops.dev/api-reference/endpoints/account/get-account): Returns a composed snapshot of the calling org's onboarding state: organization metadata, the connected AWS account and live SES send quota, the production-access request status, the CloudFormation template version, and the active plan. The response is cached per-org for 60 seconds (a `Cache-Control: max-age=60` response header advertises this), so repeated polling does not generate extra AWS or DB load. Each top-level field is always present — sub-objects are `null` when the corresponding milestone has not been reached: - `aws` and `send_quota` are `null` until the customer has connected an AWS account, and also `null` if the live SES `GetAccount` call fails (the rest of the response still renders — SES failure degrades gracefully, it does not 5xx). - `production_access` is `null` until the customer has submitted a production-access request. - `cloudformation` is `null` until the customer has applied the SendOps CloudFormation stack. `aws.sandbox` is the live SES sandbox flag (true while SES still has the AWS account in the default sandbox); this is distinct from `production_access.status`, which reflects the SendOps-side request workflow. Use this endpoint to drive onboarding UIs, detect template drift, and gate features on plan tier. ## Templates - [List email templates synced from GitHub](https://developers.sendops.dev/api-reference/endpoints/entities/list-templates): Returns the org's email templates, sourced from the GitHub repo declared in your `sendops.json` manifest. This endpoint is read-only — templates are managed by committing changes to the repo. Sorted newest-first. `version` is the short (7-char) Git commit SHA of the most recent sync. - [Get one template by slug](https://developers.sendops.dev/api-reference/endpoints/entities/get-template): Returns one template by its file name (the slug — e.g. `welcome.html`). Returns `404 not_found` when the slug is unknown in this org. ## Channels - [List channels (SES configuration sets)](https://developers.sendops.dev/api-reference/endpoints/entities/list-channels): Returns the org's channels — each backed by an AWS SES configuration set with its TLS, tracking, and suppression policy. Identities currently bound to each channel are included in the response so you can see at a glance which addresses send through which channel. Sorted newest-first. - [Get one channel by slug](https://developers.sendops.dev/api-reference/endpoints/entities/get-channel): Returns one channel by its slug. The path parameter is named `name` for legacy URL stability, but the expected value is the channel **slug** (the `slug` field from the list response), not the human display name. Returns `404 not_found` when the slug is unknown. ## Tracking Domains - [List tracking domains](https://developers.sendops.dev/api-reference/endpoints/entities/list-tracking-domains): Returns the org's CNAME-based open/click tracking subdomains and the DNS records you need to publish for each. `status` is a coalesced view across DNS-record verification and SES-identity verification: `verified` requires both checks to pass; `failed` covers DNS failures; anything else is `pending`. `verified_at` is the time of the last successful verification and stays `null` until then. This endpoint is read-only — add or remove tracking domains via the SendOps dashboard. Sorted newest-first. - [Get one tracking domain](https://developers.sendops.dev/api-reference/endpoints/entities/get-tracking-domain): Returns one tracking domain by its full subdomain string. Match is case-insensitive. `dns_records` always starts with the tracking subdomain's CNAME (pointing at the SendOps redirect host), followed by the SES DKIM CNAMEs — all of them must be published for verification to succeed. Returns `404 not_found` when the domain is not registered with this org. ## Identities - [List SES identities (email and domain)](https://developers.sendops.dev/api-reference/endpoints/entities/list-identities): Returns the SES identities (email addresses and domains) the org has adopted in SendOps. Only **managed** identities are returned — SES identities visible in your AWS account that have not been adopted in SendOps are excluded. `verification_status` and `dkim_status` today hold the same value (both are exposed so they can diverge in a future revision without breaking clients). Sorted newest-first. - [Get one identity](https://developers.sendops.dev/api-reference/endpoints/entities/get-identity): Returns one identity by its full address. Match is case-insensitive. Percent-encode `@` as `%40` (e.g. `user%40example.com`). Returns `404 not_found` when the identity is not managed by this org.