GETRead every setting/api/v1/settingsPATCHUpdate sending preferences/api/v1/settings/sendingGETRead the address book/api/v1/settings/identitiesPOSTAdd a sender address/api/v1/settings/identitiesPATCHEdit or promote an address/api/v1/settings/identities/{identityId}DELETERemove an address/api/v1/settings/identities/{identityId}PATCHUpdate sign-up settings/api/v1/settings/optinPOSTTest the confirmation email/api/v1/settings/optin/test-send
Can I send? — the one call
GET /api/v1/me answers it. Several separate guards can refuse a send, and until now they refused
without saying which one: this block names them.
bash
curl -H "x-api-key: $AGENTMAIL_API_KEY" "https://www.agentsmail.io/api/v1/me"json
{
"success": true,
"data": {
"organization": {"id": "…", "name": "Atelier Nord", "slug": "atelier-nord"},
"organizationRole": "admin",
"keyId": "…",
"sending": {
"ready": false,
"blockers": ["domain_not_verified"],
"setup": {
"status": "dns_pending",
"provisioning": "provisioned"
}
}
}
}Blocker codes
They are stable and never translated — this is a machine interface. Build your remediation on them, not on a message.| Code | What it means | How to clear it |
|---|---|---|
sending_disabled | Sending is switched off application-wide | Nothing you can do from the API |
organization_not_allowed | The organization is not on the sending allowlist | Ask the operator to allow it |
sending_paused | The email provider suspended sending for this organization | Nothing from the API — see below |
domain_not_verified | A domain is declared but its DKIM is not SUCCESS | Publish the DNS records, then verify |
domain_ownership_unproven | DKIM is SUCCESS, but this organization has not proved it owns the domain | Publish the _agentmail-challenge TXT, then verify |
sender_not_on_verified_domain | A domain is verified, but the default sender address is not on it (nor on one of its subdomains) | Promote or set an address on the verified domain |
no_sender_identity | The sender address book has no default — reported only alongside a real blocker | Add an address (POST /api/v1/settings/identities) |
sending_paused is not something you can retry. It appears when the email provider
suspends the organization — reputation, usually a bounce or complaint rate. Every send to your
contacts is refused while it is there: campaigns, sequences, opt-in confirmations. The
campaign test send stays available, so you can still check how a campaign renders while you
sort the suspension out — it goes out under the application address, not yours. The blocker
clears on its own when the provider re-enables sending, and never from the API. Fix what caused
it — clean your lists, stop importing cold contacts — and talk to the operator.no_sender_identity alone is not a blocker. With no sending identity set, emails still go out
under the application address. It appears only when something else is already blocking, to tell
you the account is bare.sender_not_on_verified_domain is a real blocker, and it is yours to fix. A verified domain
only lets you send from that domain: with codemail.io verified, hello@codemail.io and
news@news.codemail.io go out, hello@example.org does not. Without this check the provider
accepts the batch and then fails every message with "Email address is not verified" — nothing
in the product would tell you why. It only ever appears once at least one domain is verified:
before that, set the sender address freely.Setup status
sending.setup is the same state the account owner reads on screen, as one code instead of a list.
It is purely informational: it never enters blockers and never changes ready. Gate your
sends on ready, and use setup.status to say where the account stands in one word.
status is derived, first match wins, in this order:
status | When | What to do |
|---|---|---|
suspended | The email provider suspended sending for this organization | Nothing from the API — see the callout |
sending_disabled | Sending is switched off application-wide | Wait — nothing you can do from the API |
not_allowed | The organization is not on the sending allowlist | Ask the operator to allow it |
verification_failed | Sending is blocked on domains, and one of them failed DKIM verification | Re-check the DNS records, then verify |
dns_pending | Sending is blocked on domains for any other reason, including none declared | Declare a domain, publish DNS, verify |
ownership_pending | DKIM succeeded, but the domain ownership is not proved yet | Publish the challenge TXT, then verify |
sender_mismatch | A domain is verified, but the default sender address is not on it | PATCH /api/v1/settings/sending |
incomplete | Nothing blocks, but the sending isolation is not fully in place | Nothing — the operator retries it |
active | Nothing blocks, and the isolation is in place | Send |
provisioning is the raw isolation state — none, provisioned or incomplete. none is not a
fault: it simply means nothing has been attempted yet, and it does not stop a send.
An organization that is not allowed to send never reads
active. That is the whole point of
the ordering: a green light for a send the service would refuse is worse than no light at all. A
verified domain does not make an account allowed, and setup.status says so.blockers is empty, ready is true, and
setup.status is active — the account sends fine. verification_failed only ever shows up when
the failure is actually blocking.
Reading everything at once
GET /api/v1/settings returns the whole state in one call.
json
{
"success": true,
"data": {
"sending": {
"senderEmail": "bonjour@atelier-nord.fr",
"senderName": "Atelier Nord",
"postalAddress": "12 rue des Lilas\n59000 Lille",
"effectiveSender": "Atelier Nord <bonjour@atelier-nord.fr>"
},
"optin": {
"required": true,
"subject": "Confirme ton inscription",
"content": "<div>…</div>",
"customized": false
},
"readiness": {
"ready": true,
"blockers": [],
"setup": {"status": "active", "provisioning": "provisioned"}
}
}
}readiness here is the very same object GET /api/v1/me returns under sending, setup
included — one question, one answer, two places to read it.
effectiveSender is what will actually go out, fallback included — that is what you want to check,
not the column.
Sending identity
An organization keeps a sender address book: one address per verified domain, and exactly one of them marked default. The default signs everything that does not name its own address: sequences, sign-up confirmations, and every campaign that leftsendingIdentityId out. A campaign can name
one of the other addresses — see
POST /api/v1/campaigns — and then that address signs it, whatever the
default becomes.
senderEmail and senderName in GET /api/v1/settings describe the default address. The
contract has not changed: read them exactly as before. The full book lives at
GET /api/v1/settings/identities.Reading and writing the default — unchanged
PATCH /api/v1/settings/sending — admin role required.
bash
curl -X PATCH -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
-d '{"senderName":"Atelier Nord"}' \
"https://www.agentsmail.io/api/v1/settings/sending"null is cleared and falls back to the
application address. Without that distinction, setting only the display name would wipe the sender
address.
This endpoint acts on the default address, and on it alone:
| Body | Effect |
|---|---|
senderEmail (non-null) | Sets the default address — the identity is created if the book is empty |
senderEmail: null | Removes the default from the book; sending falls back to the application address |
senderName alone | Renames the default. Refused with 400 when there is no default — a display name has nowhere to live without an address |
postalAddress | Stays on the organization: a legal mention, not a sender attribute |
senderEmail: null is refused when the book holds more than one address. Removing the default
while alternatives remain would leave the account with a full book and nothing signing. Promote
another address first, with PATCH /api/v1/settings/identities/{identityId}.The address book
| Endpoint | Role | What it does |
|---|---|---|
GET /api/v1/settings/identities | member | The whole book, default first |
POST /api/v1/settings/identities | admin | Adds an address — {senderEmail, senderName?, isDefault?} |
PATCH /api/v1/settings/identities/{identityId} | admin | Changes the address or display name, or promotes it with {"isDefault": true} |
DELETE /api/v1/settings/identities/{identityId} | admin | Removes an address from the book |
bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
-d '{"senderEmail":"news@atelier-nord.fr","senderName":"Atelier Nord"}' \
"https://www.agentsmail.io/api/v1/settings/identities"json
{
"success": true,
"data": {
"id": "…",
"senderEmail": "news@atelier-nord.fr",
"senderName": "Atelier Nord",
"isDefault": false,
"createdAt": "2026-08-17T09:00:00.000Z"
}
}The default cannot be deleted while other addresses remain, and
{"isDefault": false} is
refused. Both answer 400 naming the address. Silently promoting a survivor would change the
sender of every scheduled campaign, every running sequence and every opt-in confirmation — without
anyone asking for it. Promote another address first, then delete.Deleting the last address is allowed: sending then falls back to the application address, and
no_sender_identity says so.An address named by a campaign cannot be deleted either — whatever that campaign's status,
draft and already-sent included. The refusal is a
400 that names the campaigns. The
alternative would be a scheduled mass send silently changing sender, which is precisely what this
product refuses to do. Point those campaigns at another address, or back at the default with
{"sendingIdentityId": null}, then delete.Sequences and opt-in confirmations always take the default. There is no per-sequence sender:
the choice exists on campaigns only, through
sendingIdentityId.The sender address must be on a verified domain
Every address in the book — not just the default — must live on a domain of yours that is verified and proved. Once the organization has at least one verified domain,senderEmail must be on
it, or on one of its subdomains. Anything else is refused with 400, naming both facts:
bash
curl -X PATCH -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
-d '{"senderEmail":"hello@example.org"}' \
"https://www.agentsmail.io/api/v1/settings/sending"json
{
"error": "Erreur de validation : Votre domaine codemail.io est vérifié — mais hello@example.org n’est pas dessus. Utilisez une adresse sur ce domaine ou sur l’un de ses sous-domaines."
}domain_not_verified still blocks the sends until a domain is verified, and
sender_not_on_verified_domain takes over from there.
Sign-up settings
PATCH /api/v1/settings/optin — admin role required.
bash
curl -X PATCH -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
-d '{"required":true,"subject":"Confirm your sign-up","content":"<p><a href=\"{{confirmationUrl}}\">Confirm</a></p>"}' \
"https://www.agentsmail.io/api/v1/settings/optin"| Field | Effect |
|---|---|
required | When true, the organization forces double opt-in: doubleOptIn: false sent to the contact API no longer has any effect |
subject | Subject of the confirmation email |
content | Its HTML |
The content must contain
{{confirmationUrl}}. Without it the recipient has no way to
confirm, and double opt-in breaks silently. A content without it is refused with 400, naming the
missing variable.{{confirmationUrl}} (required), {{firstName}}, {{lastName}}, {{email}},
{{postalAddress}}, {{currentYear}}. There is no unsubscribe variable — the recipient is not
subscribed yet.
Sending the default content back is not a customization. A content identical to the default is
stored as empty, so the organization keeps inheriting future improvements to it. customized tells
you which case you are in.
Testing the confirmation email
POST /api/v1/settings/optin/test-send — admin role required.
bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
-d '{"email":"you@example.com","content":"<p><a href=\"{{confirmationUrl}}\">Confirm</a></p>"}' \
"https://www.agentsmail.io/api/v1/settings/optin/test-send"content and subject of the body win over what is stored: try before you publish. The
confirmation link in a test is fake — testing three times must not put three real, valid tokens
into circulation.
A test is a real send: it passes the same guards as any other. Answers {"status":"blocked"}
(sending switched off application-wide), {"status":"not_allowed"} (organization not allowed, or
domain not verified) or {"status":"send_refused"} (the provider refused the send) rather than
failing, so you can tell a refusal from an error — and tell the refusals apart.