Seed lists
A seed list is a set of mailboxes run by a deliverability vendor (Warmy, for example). The seeds receive your real broadcasts alongside your contacts, so the vendor can report whether your mail lands in the inbox, promotions or spam. Seeds are not contacts: they never appear in contact lists, segments or campaign reports.
Vendors rotate their seed mailboxes, often monthly. This API is how you keep a list current without pasting a new file into Settings each time: a script, a scheduled job, or an automation tool that picks up the vendor’s new list and sends it here.
The API covers the addresses on a list. Creating a list, setting its rotation period, and choosing which senders carry it are done in the app, under Settings › Deliverability › Seed lists. That is also where you find each list’s id.
Required scopes#
| Endpoint | Scope |
|---|---|
GET /api/seed-lists | seed-lists:read |
GET /api/seed-lists/{id}/seeds | seed-lists:read |
PUT /api/seed-lists/{id}/seeds | seed-lists:write |
POST /api/seed-lists/{id}/seeds/changes | seed-lists:write |
seed-lists:write implies seed-lists:read. None of these send mail.
List seed lists#
GET /api/seed-lists
{
"data": [
{
"id": "8b0c…",
"name": "Warmy seeds",
"vendor_label": "Warmy",
"rotation_days": 30,
"rotation_grace_days": 5,
"refreshed_at": "2026-09-01T14:02:11.000Z",
"rotates_at": "2026-10-01T14:02:11.000Z",
"stale_at": "2026-10-06T14:02:11.000Z",
"freshness": "fresh",
"counts": { "active": 412, "paused": 0, "retired": 38, "conflict": 1 },
"sender_count": 2,
"…": "…"
}
],
"next_cursor": null,
"has_more": false
}
freshness is where the list stands against its rotation period:
| Value | Meaning |
|---|---|
never_synced | No addresses have been added yet. |
no_rotation | The list has no rotation period, so it never goes stale. |
fresh | Not yet due. |
due_soon | Within 5 days of rotates_at, or the last half of a shorter rotation period. |
grace | Past rotates_at, before stale_at. Still sent to. |
stale | Past stale_at. Broadcasts stop sending to this list until it is updated. |
Any successful update, whether from Settings or this API, restarts the clock:
refreshed_at becomes now, and rotates_at and stale_at move with it.
List the addresses on a seed list#
GET /api/seed-lists/{id}/seeds?status=active&limit=100&offset=0
Offset-paged, ordered by address, with total in the response (see
Lists & pagination). Filter with status
(active, paused, retired, conflict) and q, a substring of the
address.
Each address has a status:
active: sent to with broadcasts.paused: kept on the list but not sent to. Only set in the app.retired: removed by an update, or retired in the app. Kept with its history, never deleted. An address retired in the app (status_reason: "retired_by_user") or after a bounce saying its mailbox no longer exists ("hard_bounce") stays retired even if a later update includes it again. Only restoring it in the app brings it back. A bounce where the mailbox provider refused the mail (a block or policy rejection) doesn’t retire the address, because the mailbox is still there.conflict: the address is also a contact in this workspace. A contact always wins, so it is held and never sent to as a seed. Every update re-checks the seeds already on the list. One that has since become a contact is held, and one whose contact is gone is released.
Replace the addresses#
PUT /api/seed-lists/{id}/seeds
Send the vendor’s whole current list. This is the natural fit for a monthly rotation.
curl -X PUT "https://api.radarsend.com/t/acme/api/seed-lists/{id}/seeds" \
-H "X-API-Key: $API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"seeds": [
{ "email": "seed-0001@gmail.com" },
{ "email": "seed-0002@outlook.com" },
{ "email": "seed-0003@yahoo.com", "provider": "yahoo" }
]
}'
- Addresses not in
seedsare retired. - Retired addresses that come back are reactivated, unless they were
retired in the app or after a hard bounce (counted in
kept_retired). - Addresses already on the list are left as they are, including any paused in the app.
- Addresses are lowercased and deduplicated. Entries that aren’t valid email
addresses are ignored and counted in
invalid. provideris optional. It is kept only when it is one ofgmail,google_workspace,outlook,microsoft365,yahoo,aol,icloud,protonorother; otherwise it is inferred from the domain. Name it for Microsoft 365 and Google Workspace mailboxes: they sit on custom domains, so the domain alone reads asotheruntil RadarSend reads the domain’s mail servers. Stating a provider for an address already on the list updates it (counted inproviders_changed); an inferred one, orother, never overwrites it.
One request can carry up to 10,000 addresses.
Add or remove addresses#
POST /api/seed-lists/{id}/seeds/changes
Send only what changed, for example a vendor’s changed-seeds export. Everything not named is left alone.
{
"add": [{ "email": "seed-0419@gmail.com" }],
"remove": ["seed-0007@gmail.com"]
}
remove retires addresses. An address in both add and remove is kept.
Send add, remove, or both. A request with neither is refused with
400 INVALID.
The response#
Both writes return what the update did:
{
"applied": true,
"sync_id": "c41e…",
"added": 12,
"reactivated": 1,
"retired": 13,
"unchanged": 399,
"conflicts": 1,
"invalid": 0,
"kept_retired": 0,
"providers_changed": 0,
"sendable_before": 412,
"sendable_after": 411,
"large_change": false,
"samples": {
"added": ["seed-0419@gmail.com", "…"],
"retired": ["seed-0007@gmail.com", "…"],
"conflicts": ["jane@customer.example"],
"invalid": []
},
"list": { "id": "8b0c…", "freshness": "fresh", "…": "…" }
}
samples holds up to 10 addresses of each kind. Every update is recorded in
the list’s history in Settings, marked as an API update.
Dry runs#
Add ?dry_run=true to either write to see what it would do without changing
anything. The response has the same shape, with applied: false,
sync_id: null and no list.
Large changes#
An update that would leave fewer than half of the active and paused seeds
is refused unless you add ?allow_large_change=true:
{
"error": {
"code": "LARGE_CHANGE",
"message": "This would leave 2 of 412 seeds. Confirm to apply it.",
"details": { "retired": 410, "sendable_before": 412, "sendable_after": 2, "large_change": true, "…": "…" }
}
}
The status is 409. This guards against an empty or truncated file wiping out
a list. details carries the full would-be response.
The rule measures what is left, not what is retired. A vendor’s monthly
rotation, where every address is swapped for a new one, retires everything but
doesn’t shrink the list, so it goes through without the flag. If the vendor
really did cut the list in half, send the request again with
allow_large_change=true.
Retries#
Updates are safe to retry. A replace compares by address, and a change skips addresses already added or already retired, so sending the same update twice leaves the addresses unchanged the second time. It still counts as an update: it restarts the rotation clock (the vendor’s list was confirmed current) and appears in the list’s history. Two updates to the same list never interleave: the second waits for the first.
Only send a list you have just fetched from the vendor. Re-sending an old, cached copy keeps the list looking current after the vendor has rotated its mailboxes, and broadcasts would then mail addresses that no longer exist.
Errors#
| Status | Code | When |
|---|---|---|
400 | INVALID | A change with neither add nor remove. |
400 | TOO_MANY | More than 10,000 addresses in one request. |
404 | NOT_FOUND | No seed list with that id in this account. |
409 | LARGE_CHANGE | See Large changes. |
409 | VENDOR_MANAGED | The list is connected to its vendor (for example Warmy) in Settings and updates itself from there; its addresses can’t be changed through the API. Disconnect the vendor to manage the list yourself. |
See Errors & rate limits for the envelope and the codes every endpoint shares.