List undeliverable recipients (permanent failures + exclusions)

Search Documentation

Search across all developer documentation

undeliverable

List undeliverable recipients (permanent failures + exclusions)

GET /v1/undeliverable
Auth required api.undeliverable.view

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.

200 Paginated list of undeliverable rows application/json
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
401 Missing, malformed, or unknown API key application/problem+json
403 Key lacks the required scope or plan limit violated application/problem+json
422 Query parameter or path value failed validation application/problem+json
429 Per-org rate limit exceeded application/problem+json
500 Unexpected server-side failure. The code is internal_error. The request_id field can be quoted to SendOps support to investigate. application/problem+json