API

Everything the app does to an audience, a template or a campaign is reachable over HTTP. A script — or an agent — works on the same data as the interface, with the same rules, without a browser session. This page is the map: authentication, the shape of every answer, and the full list of endpoints. The details live in the topic pages.
TopicWhat it covers
DiscoveryWhere the key stands, and the lists it can work on
AudienceContacts and tags
Content & SendTemplates, rendering, campaigns and sending
Recipient linksThe token URLs that live inside sent emails
Settings & domainsSending identity, sign-up, sending domains and DNS

Authentication

Every /api/v1 request carries an API key in the x-api-key header. A key is created from the app and belongs to one organization: that organization is the entire scope of the key.
curl -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/tags"

The key acts as the person who created it

A key is not a second identity with its own powers. It carries the rights of the member who created it, in the organization it was created for. A key made by an admin can write; a key made by a member can read. Nothing is granted through the API that the same person could not do in the interface.
A key stops working when its owner leaves the organization. No orphan access survives a departure: the key answers 401 from that moment, exactly as if it had been revoked. Revoking the key of someone who left is therefore a cleanup, not a security fix.
Two rules decide every call, and both have to pass:
QuestionDecided byAnswer when it fails
Is the caller still a member, with which role?The key's owner and their role401 / 403
Does the resource belong to the key's organization?The key's organization404

Who can do what

OperationMinimum role in the organization
Reading anything, and POST /api/v1/rendermember
Creating, updating, deleting, tagging, sendingadmin
POST /api/v1/render is the exception among the write verbs because it writes nothing: it renders and returns, with no side effect.

Response envelope

CaseBody
Success{"success": true, "data": …}
Paginated list{"success": true, "data": [...], "pagination": {"total", "page", "limit", "totalPages"}}
Error{"error": "…"}
Validation error{"error": "…", "details": [ … Zod issues … ]}
StatusWhen
200Read or mutation done
201Resource created
202Send accepted, happening in the background — nothing is finished yet
400Invalid parameters or body — details lists the offending fields
401Missing or invalid key, or a key whose owner left the organization
403The caller is a legitimate member, but their role is too low
404Unknown resource, or a resource of another organization
409State conflict — a tag name already taken, a campaign already sent
429Rate limit of the key exceeded
500Unexpected server error
A resource of another organization answers 404, never 403. A 403 would confirm that the id exists. Same response for an unknown id and for an id you are not allowed to read — you cannot tell them apart, and that is the point. The 403 is reserved for the other question: you are in the right organization, your role is too low.

Every endpoint

Discovery — details

EndpointWhat it does
GET /api/v1/meThe organization of the key, its role, the key's id
GET /api/v1/listsLists of the organization with their contact count, paginated
GET /api/v1/lists/{listId}One list, without the contact count

Audience — details

EndpointWhat it does
GET /api/v1/contactsContacts of a list, paginated and filterable
GET /api/v1/contacts/{contactId}One contact with its tags
PATCH /api/v1/contacts/{contactId}Updates first name, last name or status
GET /api/v1/listsLists of the organization with their contact count
GET /api/v1/lists/{listId}One list
POST /api/v1/listsCreates a list
POST /api/v1/lists/{listId}/contactsAdds a contact to a list (upsert)
POST /api/v1/lists/{listId}/contacts/bulkPushes up to 500 contacts at once, line by line
POST /api/v1/contacts/{contactId}/unsubscribeUnsubscribes a contact
POST /api/v1/contacts/{contactId}/resubscribePuts a contact back to subscribed
GET /api/v1/tagsTags of the organization with their carrier count
POST /api/v1/tagsCreates a tag
POST /api/v1/contacts/{contactId}/tagsPuts a tag on a contact, by name (get-or-create)
DELETE /api/v1/contacts/{contactId}/tags/{tagId}Removes a tag from a contact

Content & Send — details

EndpointWhat it does
GET /api/v1/templatesTemplates of the organization, paginated
GET /api/v1/templates/{templateId}One template with its HTML
POST /api/v1/templatesCreates a template
PATCH /api/v1/templates/{templateId}Updates name, subject or HTML
DELETE /api/v1/templates/{templateId}Deletes a template
POST /api/v1/templates/{templateId}/test-sendSends one test email of the template, writes nothing
POST /api/v1/renderRenders HTML without sending anything
GET /api/v1/campaignsCampaigns of the organization, paginated
POST /api/v1/campaignsCreates a campaign in draft
GET /api/v1/campaigns/{campaignId}State, target, recipient count and send progress
GET /api/v1/campaigns/{campaignId}/statsDelivery, opens, clicks and unsubscribes of a campaign
PATCH /api/v1/campaigns/{campaignId}Edits a draft — HTML, subject, name or target
POST /api/v1/campaigns/{campaignId}/sendTriggers the send, now or at a chosen time — answers 202
POST /api/v1/campaigns/{campaignId}/test-sendSends one test email, writes nothing

Settings & domains

Detailed in Settings & domains.
EndpointWhat it does
GET /api/v1/settingsSending identity, sign-up settings and readiness, in one read
PATCH /api/v1/settings/sendingSets sender address, display name and postal address
PATCH /api/v1/settings/optinSets double opt-in and the confirmation email
POST /api/v1/settings/optin/test-sendSends a test of the confirmation email, with a fake link
GET /api/v1/domainsDeclared domains, verification status and the DNS records to publish
POST /api/v1/domainsDeclares a domain and creates its SES identity
GET /api/v1/domains/{domainId}One domain with its DNS records
POST /api/v1/domains/{domainId}/verifyRe-checks the domain against SES and stores the status
DELETE /api/v1/domains/{domainId}Removes the declaration

Rate limit

The limit is carried by the key, not by the IP. A key over its limit answers 429 on every endpoint. Back off and retry: nothing was done.

Where to start

Start with Discovery: two calls turn a bare key into a context — the organization it opens, and the listId every audience endpoint asks for. Then read the audience — a campaign without a list to send it to goes nowhere — and compose with templates and rendering. The whole path from an empty organization to a sent newsletter is walked end to end at the bottom of Content & Send.