Discovery API

An agent starts with one thing: an API key. Which organization it opens, what it is allowed to do, which lists exist behind it — none of that is written on the key. These two endpoints answer it. GET /api/v1/me says where the key stands, GET /api/v1/lists says what there is to work on. Together they turn a key into a context, and every other endpoint of the API becomes reachable without a human pasting an id out of the interface.

Authentication

Same key, same rules as everywhere else: an x-api-key header, one organization per key. Authentication, roles, the response envelope and the full list of status codes are on the API reference.
curl -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/me"
Everything on this page reads and nothing writes, so being a member of the organization is enough.

Where the key stands

GET /api/v1/me

The first call of an integration. It answers the one question a key alone cannot: whose data am I about to touch, and with what rights?
FieldDescription
organization.idThe organization the key belongs to — the entire scope of the key
organization.nameIts display name
organization.slugIts slug, the one that appears in the app URLs
organizationRoleThe role the key acts with: owner, admin or member
keyIdThe identifier of the key itself
curl -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/me"
{
  "success": true,
  "data": {
    "organization": {"id": "…", "name": "Acme", "slug": "acme"},
    "organizationRole": "admin",
    "keyId": "…"
  }
}
The value of the key is never returned. keyId names the key, it does not let anyone authenticate with it — a /me response that leaks is an organization name that leaks, not a credential. No endpoint of /api/v1 ever echoes a secret back.
role is the useful half of the answer. A key made by a member can read and render, nothing more; admin is what creating, tagging and sending require — the table is on the API reference. Reading the role before the first write is how an agent finds out it is read-only, rather than by collecting a 403 halfway through a run it cannot undo.

Telling a production key from a test one

A key carries no label saying which environment it belongs to. What it carries is an organization, and that is the thing to assert: compare organization.slug — or organization.id — against what your integration expects, and stop if they differ.
ORG=$(curl -s -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/me" | jq -r '.data.organization.slug')

[ "$ORG" = "acme-staging" ] || { echo "wrong organization: $ORG"; exit 1; }
One call, at start-up, and it is the whole distance between a rehearsal and a real audience. An agent that skips it finds out where it was pointed after the send.

Finding a list

GET /api/v1/lists

The lists of the organization, paginated, each with the number of contacts it holds. This is where a listId comes from: the contact endpoints all require one, and reading it off the app was until now the only way to get it.
ParameterInRequiredDescription
pagequerynoDefaults to 1
limitquerynoDefaults to 20, capped at 100
searchquerynoFilters by list name, partial and case-insensitive
curl -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/lists?limit=50"
{
  "success": true,
  "data": [
    {
      "id": "…",
      "name": "Newsletter",
      "contactCount": 1248,
      "createdAt": "…",
      "updatedAt": "…"
    }
  ],
  "pagination": {"total": 3, "page": 1, "limit": 50, "totalPages": 1}
}
pagination is the same envelope as everywhere else — total counts the lists, not the contacts. The organization is not a parameter and never will be: it comes from the key, so this endpoint can only ever return your own lists.
contactCount is not the size of an audience. It counts every contact of the list, whatever its status — unsubscribed, bounced and complained included. How many people a campaign would actually reach is recipientCount on the campaign, which counts subscribers only — see Content & Send.
Creating a list sits on the other side, with the endpoints that feed it: POST /api/v1/lists is documented in Audience.

GET /api/v1/lists/{listId}

One list. Without contactCount : le détail ne le calcule pas, seule la collection le porte. Si tu as besoin du compteur pour une seule liste, lis-le dans GET /api/v1/lists.
curl -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/lists/$LIST_ID"
StatusWhen
200The list, without its contact count
404Unknown id, or a list belonging to another organization
A list of another organization answers 404, never 403. The two cases are indistinguishable by design: a 403 would confirm that the id exists somewhere, which already says something about an account that is not yours. Do not read this 404 as "wrong permissions" — read it as "not in your organization", and stop looking.

The bootstrap sequence

1

Find out where you are

curl -s -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/me" | jq '.data'
Assert the organization is the one you meant, and read role: member means every write below will answer 403.
2

Pick the list

LIST_ID=$(curl -s -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/lists" | jq -r '.data[] | select(.name == "Newsletter") | .id')
An empty answer means the organization has no list yet — create one with POST /api/v1/lists, see Audience.
3

Work the audience

curl -s -H "x-api-key: $MK_API_KEY" \
  "https://agentsmail.io/api/v1/contacts?listId=$LIST_ID&status=subscribed"
Everything about contacts and tags — reading, adding, importing a batch, unsubscribing — is on Audience. GET /api/v1/tags is the same trick as /lists, for tag ids.
4

Compose and send

curl -s -X POST -H "x-api-key: $MK_API_KEY" -H "content-type: application/json" \
  -d '{"name":"August newsletter","subject":"What shipped in August","templateId":"'$TEMPLATE_ID'","listId":"'$LIST_ID'"}' \
  "https://agentsmail.io/api/v1/campaigns"
Templates, rendering, campaigns and the send itself are on Content & Send, which walks the whole path end to end.
Two calls of context, and the rest of the API answers. Nothing here is stored on your side: an agent that starts every run with /me and /lists never works from a stale id.