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. SND-713: the three windowed reasons
(repeated_transient, undetermined, soft_bounce_accumulation)
are configurable per org; callers that filter on them should
confirm via GET /v1/undeliverable/rules that the rule is
enabled before relying on results.
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
Errors follow the RFC 7807 problem format — see the error reference.
data array<object> required Each entry in data:
email string<email> required status string enum required listed — the address currently fails our classification
rules. excluded — an operator has cleared the address;
callers should remove it from their local suppression list.
One of: listed, excluded
last_changed_at string<date-time> required Most-recent state transition. For listed rows this is the
timestamp of the most recent qualifying event
(last_seen_at). For excluded rows it is excluded_at. Use
the largest value from your processed batch as the next
?since= cursor.
reason string enum optional Populated only for status: "listed". SND-713 added the three
windowed reasons (repeated_transient, undetermined,
soft_bounce_accumulation) — older clients that switch on the
original three values should expect new ones and handle them
as "currently classified as undeliverable, see rules".
One of: permanent_bounce, complaint, rejected, repeated_transient, undetermined, soft_bounce_accumulation
subtype string optional Bounce sub-type (NoEmail, General, ...) for permanent
bounces, or the complaint feedback type for complaints.
Empty for rejects. Listed rows only.
first_seen_at string<date-time> optional Oldest qualifying event for this address in the window. Listed rows only.
last_seen_at string<date-time> optional Most recent qualifying event for this address. Listed rows only.
event_count integer<int64> optional Number of qualifying events in the window. Listed rows only.
Constraints: 0–∞
last_diagnostic string optional SMTP response from the most recent event. Useful for triage. Listed rows only.
last_sender_identity string optional SES identity (From address) that last hit this recipient. Listed rows only.
excluded_at string<date-time> optional When the operator cleared the address. Excluded rows only.
excluded_by string<email> | null optional Operator email. Null when the operator has been removed from the org since. Excluded rows only.
exclusion_reason string optional Free-text operator note recorded at exclusion time. Excluded rows only.
pagination object required Fields of pagination:
has_more boolean required next_cursor string | null optional code is internal_error. The
request_id field can be quoted to SendOps support to investigate.
application/problem+json