Add or update one contact

POST /api/v1/lists/{listId}/contacts Adds a contact to a list, or completes the one already there — the email address is the key, inside that list. This is the endpoint a funnel or a lead magnet calls: unlike the bulk import, it emits side effects, so an enrolment event fires and a double opt-in confirmation goes out when the organization requires one.

Authorization

Role in the organizationThis endpoint
OWNERAllowed
ADMINAllowed
MEMBERRefused — 403
A key carries the role its creator holds in this organization — never a role on AgentsMail itself. See API keys.

Path parameters

ParameterTypeDescription
listIdstringRequired. The list the contact joins. A contact belongs to exactly one

Request body

FieldTypeRequiredDescription
emailstringyesThe upsert key, together with the list. Must be a valid address
firstNamestringnoUp to 100 characters
tagsstring[]noTag names, 1 to 50 characters each. Created in the organization if they do not exist yet
doubleOptInbooleannoLegacy field, accepted but ignored. The organization sign-up setting alone decides whether confirmation is required
json
{
  "email": "ada@example.com",
  "firstName": "Ada",
  "tags": ["vip", "webinar-march"]
}

Example

bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "ada@example.com", "firstName": "Ada", "tags": ["vip"]}' \
  "https://www.agentsmail.io/api/v1/lists/$LIST_ID/contacts"

Response

  • 200 OK — the contact, created or completed. Not 201: the semantics are an upsert, and you cannot tell from the call which of the two happened.
  • 400 Bad Request — invalid address, a tag over 50 characters, a malformed body; details carries the field-level issues.
  • 401 Unauthorized — missing or invalid key, or its owner left the organization.
  • 403 Forbidden — the key's owner is a member of the organization, not an admin.
  • 404 Not Found — no such list, or it belongs to another organization. The API never confirms that an id exists to a caller who has no right to it.
  • 429 Too Many Requests — over 120 requests in a minute for this key.

Response fields

FieldTypeDescription
successbooleanIndicates if the operation was successful
data.idstringUnique identifier for the contact
data.emailstringIts address — unique inside its list
data.firstNamestring | nullWhat you know of the person
data.lastNamestring | nullNever set by this endpoint; carried from an import or the interface
data.statusstringpending, subscribed, unsubscribed, bounced, complained
data.listIdstringThe list it belongs to
data.listobject{id, name} of that list
data.tagsarray{id, name} of every tag carried after the call
data.createdAtstringISO creation date
data.updatedAtstringISO date of the last change

Example response

json
{
  "success": true,
  "data": {
    "id": "4d19…",
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": null,
    "status": "subscribed",
    "listId": "0b7c…",
    "list": {"id": "0b7c…", "name": "Newsletter"},
    "tags": [{"id": "7c2e…", "name": "vip"}],
    "createdAt": "2026-08-23T10:22:05.000Z",
    "updatedAt": "2026-08-23T10:22:05.000Z"
  }
}

Notes

  • An existing contact is completed, never reset. Its status is untouched: someone who unsubscribed stays unsubscribed, whatever this call carries. Putting a contact back to subscribed is an explicit act — see Resubscribe a contact.
  • A contact still pending gets a new confirmation email. When the organization requires confirmation and the address is already in the list as pending, the call sends a fresh link and the previous one stops working — at most once every 10 minutes per contact. The response is the same as for any other upsert. A subscribed or unsubscribed contact receives nothing. See Sign-up and double opt-in.
  • The organization setting alone decides the status of a new contact. Enabled: pending with a confirmation email. Disabled: subscribed without a confirmation email, even if doubleOptIn: true is supplied. The legacy boolean is accepted but ignored.
  • lastName is not accepted here. The single upsert takes firstName only; a full name comes in through Import contacts in bulk or the interface.
  • Tags are added, never replaced: a contact keeps the labels it already carried.
  • A deleted list answers 404. Nothing can enter a list you removed — point the call at a live list, or create one.