List undeliverable recipients (permanent failures + exclusions)
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=<RFC 3339>, 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.
Query parameters
limit integer optional Page size (1–200). Default 50.
cursor string optional Opaque cursor returned from the previous page.
since string<date-time> optional RFC 3339 timestamp. Returns only rows whose
last_changed_at >= since. Use the largest last_changed_at
from your most-recent processed batch as the next value.
The boundary is inclusive on the second.
status string enum optional Filter by row status. Default depends on ?since=: when
since is set, both listed and excluded are returned
(so callers see operator-clearing tombstones); when since
is absent, only listed rows are returned (snapshot mode).
Pass all to override the snapshot default.
reason string enum optional Filter listed rows by classification reason. Has no effect on
excluded tombstones.
Accepted values (all six are valid query inputs regardless of
whether the corresponding rule is currently enabled for the
org — filtering on a disabled rule simply returns no rows):
- permanent_bounce — locked, always on
- complaint — locked, always on
- rejected — locked, always on
- repeated_transient — configurable (SND-713)
- undetermined — configurable (SND-713)
- soft_bounce_accumulation — configurable (SND-713)
Callers filtering on a configurable reason should confirm via
GET /v1/undeliverable/rules that the rule is enabled before
treating an empty page as "no matches" rather than
"rule off".
email string optional Case-insensitive substring match on the recipient address.
min_events integer optional Only return listed rows whose event_count >= min_events.
Useful for callers who want "address that bounced ≥ N times"
policies. Default 1 (everything qualifying). Excluded
tombstones are dropped when min_events > 1.
Responses
code is internal_error. The
request_id field can be quoted to SendOps support to investigate.
application/problem+json