Skip to content

Send contacts and events from your app

The AguMail API is on every plan, the free one included. Use it to add people to your list from your own website or app, tag them, check who is subscribed, and record what they do, so a segment can count it and a flow can start from it.

Selling online? Orders, carts and products are the store half of the API, on the Commerce plans. Connect any other store walks through it.

In AguMail, go to Settings → Store & API → API keys → Create key and choose Other store or your own code. Copy the key; it is shown once. Every request below sends it as a header:

Authorization: Bearer agm_…
Content-Type: application/json
POST /api/v1/contacts
{ "contacts": [
{ "email": "sam@example.com", "firstName": "Sam", "lastName": "Rivera",
"tags": ["Newsletter", "Yoga"],
"customFields": { "plan": "Monthly", "studio": "Riverside" },
"consent": { "optedIn": true, "source": "website" } }
] }
  • Only send optedIn: true, or a consentedAt date, for people who agreed to marketing email. A new contact sent without either is saved as Never subscribed: they are on your list, and your campaigns and flows do not email them.
  • Sending an address AguMail already has updates that contact. Nobody is added twice.
  • tags adds each tag the contact does not have yet, and a flow that starts on Tag Added starts for them. Up to 20 tags per contact.
  • customFields are your own fields, up to 30 per contact. Segments can filter on them and emails can print them.
  • timezone (for example "Europe/Lisbon") lets a campaign arrive at the same local time for everyone.

The response counts what happened: { "ok": true, "created": 1, "updated": 0, "suppressedSkipped": 0, "overCapacity": 0, "confirmationsSent": 0, "unknownContacts": 0, "conflicts": 0, "conflicted": [] }. suppressedSkipped counts people who unsubscribed or can no longer be emailed; sending them again does not resubscribe them. overCapacity counts new contacts your plan had no room for. confirmationsSent counts people who had unsubscribed, asked to come back, and were sent an email to confirm it. They are in suppressedSkipped too, because they stay unsubscribed until they click the link in it. The last three are about your app’s user ids, below.

If your own app keeps a “send me emails” checkbox, ask AguMail where each person stands and keep the two in step:

POST /api/v1/contacts/status
{ "emails": ["sam@example.com", "alex@example.com"] }
{ "ok": true, "contacts": [
{ "email": "sam@example.com", "userId": null, "status": "active", "subscribed": true },
{ "email": "alex@example.com", "userId": null, "status": null, "subscribed": false }
] }

subscribed is the one to act on: it is true only for someone who is subscribed and has not unsubscribed or bounced since. status is null for an address that is not on your list. The answer covers exactly the addresses you asked about, up to 500 at a time.

Anything your business records can be sent as an event: a booked appointment, a finished course, a renewed membership.

POST /api/v1/events
{ "events": [
{ "email": "sam@example.com", "metric": "Booked appointment",
"occurredAt": "2026-09-19T10:15:00Z", "uniqueId": "appt_991", "value": 49,
"properties": { "service": "Cut & finish", "staff": "Ana" } }
] }
  • metric is your own name for what happened. Spelling and capitals do not matter: “Booked appointment”, “booked_appointment” and “Booked Appointment” are one event.
  • uniqueId is your name for this particular occurrence, such as an appointment id or an invoice number. Send it and you can safely retry: the same id is never recorded twice, and nobody is enrolled in a flow twice for it.
  • occurredAt is when it happened, not when you sent it. Leave it out and AguMail uses now.
  • properties is anything you want the email to be able to say. Up to 30 keys.
  • value is money, if the event is worth some.

Then, in AguMail:

  • Segment on it. In the segment builder pick Did something…, choose your event, and set a count and a window, such as “at least 1 time in the last 30 days”. Add narrow by property to ask about one of your own properties, for example service is Cut & finish.
  • Start a flow with it. In the flow editor choose the Any recorded event trigger and pick your event. It appears in that list once the first one has arrived.
  • Print it in the email. {{event.service}} and {{event.staff}} render what that contact’s event carried. A missing property renders as nothing rather than breaking the email.

The response counts what happened: { "ok": true, "created": 1, "duplicates": 0, "unknownContacts": 0 }. Watch unknownContacts: an event for someone AguMail does not hold is skipped rather than creating a contact, because an event is not permission to email someone. Send the person to /api/v1/contacts first.

If your app gives each user an id, send it as userId beside the email. AguMail then knows the person by that id, so when they change their email in your app, your next call moves the same contact to the new address. Their status, tags, history and flows stay with them, and they are not counted twice.

POST /api/v1/contacts
{ "contacts": [
{ "userId": "user_8412", "email": "sam.rivera@example.com" }
] }
  • userId is your own id for the person, any text from 1 to 200 characters. It is set once: a contact keeps the first id it is sent with.
  • A contact AguMail already has by email takes the id the first time you send both.
  • With a userId, the email is optional. A record with only a userId updates that contact. If nobody has that id it is skipped and counted in unknownContacts, because a new contact needs an email.
  • A new address that is on your suppression list stays blocked. The contact keeps its status, and nothing is sent to that address.
  • The unsubscribe link in an email sent before the change still works, and unsubscribes the same contact.

Two kinds of record are refused, so that two people are never mixed up: a new email that already belongs to another contact (email_taken), and an email whose contact already has a different id (user_id_mismatch). conflicts counts them, and conflicted lists each one by its place in your request, starting from 0:

{ "ok": true, "created": 0, "updated": 1, "suppressedSkipped": 0, "overCapacity": 0,
"confirmationsSent": 0, "unknownContacts": 0, "conflicts": 1,
"conflicted": [
{ "index": 1, "userId": "user_77", "email": "alex@example.com", "reason": "email_taken" }
] }

Events and the status check take the id too:

  • An event can carry userId in place of email, or beside it. With a userId, the id alone says who the event is about. An event never changes anyone’s email or id.
  • /api/v1/contacts/status takes userIds beside emails, up to 500 in all. It answers the emails first and then the ids, and every answer carries both email and userId.

When someone deletes their account in your app, delete them from AguMail too:

POST /api/v1/contacts/delete
{ "contacts": [
{ "userId": "user_8412" },
{ "email": "alex@example.com" }
] }
{ "ok": true, "deleted": 1, "notFound": 1, "conflicts": 0, "conflicted": [] }
  • Name each person by userId, email, or both, up to 500 in one request. It is a POST because some tools drop the body of a DELETE request.
  • The contact is deleted with its tags, custom fields and history. Any flow they are in stops for them at once, and they stop counting toward your plan straight away.
  • notFound counts people AguMail does not have, including anyone you already deleted. It is never an error, so you can safely send the same request again.
  • With both userId and email, the two must belong to the same contact. If they belong to two different contacts, neither is deleted, and the record is listed in conflicted with one of the reasons above.
  • Unsubscribes are kept. AguMail remembers by address who unsubscribed, marked an email as spam or bounced. If you add the same email again later, someone who unsubscribed stays unsubscribed and is counted in suppressedSkipped. Someone who never unsubscribed can be added again like anyone new.
  • Campaign and flow reports still count the emails the person was sent.

When you first connect, you can send the people and events you already have, so segments start from the whole picture. Mark those batches as history with "backfill": true:

POST /api/v1/contacts
{ "backfill": true,
"contacts": [
{ "email": "sam@example.com", "customerSince": "2024-03-02T09:00:00Z",
"consent": { "optedIn": true, "source": "website" },
"consentedAt": "2024-03-02T09:00:00Z" }
] }

The flag works the same on /api/v1/events. History is stored like any other data, but it starts no flows: nobody gets a welcome or an event-triggered email for something that happened before it reached AguMail.

customerSince becomes the contact’s Date added when AguMail creates them, so flows timed from Date added do not treat history as new. consentedAt is the date they opted in to marketing email. Send it only for people who opted in, because AguMail counts it as an opt-in.

AguMail applies the same rule without the flag: an event whose occurredAt, or a contact whose consentedAt, is more than 72 hours old is treated as history anyway. Leave the flag off for what happens from now on, so your flows can react to it.

  • 120 requests a minute per key, and up to 500 records per request.
  • 400 means a field is missing or malformed; the message names it.
  • 401 means the key is missing, wrong or revoked.
  • 402 means the request is part of the store half of the API (/api/v1/connect, orders, carts and products), which is on the Commerce plans. Everything on this page works on every plan.
  • 429 means you are over the rate limit. The Retry-After header, and retryAfter in the body, say how many seconds to wait before you send the request again.