mailtanimailtani

Events & Automations

Fire lifecycle events to trigger flow campaigns. When an event is received, any active flow with a matching trigger will automatically enroll the contact.

Fire an Event

POST
/events

Fires a lifecycle event for a contact. The contact must already exist in your project — if not, a 404 is returned. Any active flow campaign with a matching event trigger will enroll the contact.

Body

email

string

The contact's email address. Used to identify the contact; the contact must already exist in the project.

Required
event_name

string

The event name (e.g. `user.registered`, `trial.expiring`). Must match the trigger configured on your flow.

Required
properties

object

Optional key-value pairs passed to the flow. Values are also persisted as custom field values on the contact (if the corresponding custom field exists), making them available as `{{custom.fieldName}}` in any future email sends.

Optional

Example

curl -X POST https://mailtani.com/api/v1/events \
  -H "Authorization: Bearer mk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "event_name": "user.registered",
    "properties": {
      "firstName": "Jane",
      "plan": "pro"
    }
  }'

Response 202 Accepted

{
  "data": {
    "received": true
  }
}

Response 404 Not Found — if the contact does not exist

{
  "error": {
    "message": "Contact not found.",
    "code": "not_found"
  }
}

Properties and Custom Fields

Properties passed in the event payload are persisted as custom field values on the contact at the time the event fires. This means if you pass dashboardUrl in a user.registered event, {{custom.dashboardUrl}} will resolve correctly in any email sent by that flow — even if the contact was just created.

Custom fields must exist in your project for values to be stored. Create them via Settings > Custom Fields in the dashboard, or via the API.

If a field is missing: the property is discarded, and the response's dropped_properties (MCP track-event-tool) lists it. Flow emails and branches that use it are held rather than sent with the fallback, and the project owner is emailed the first time it happens. Fix it from the flow page (Fix missing fields) or the MCP (list-missing-fields-tool, backfill-custom-values-tool, resume-held-sends-tool): create the field, restore the values from past events, then send the held emails. Held emails that aren't fixed go out with their fallback after 3 days. A field your app never sends isn't held; it just uses its fallback.

Testing an Event Before You Go Live

You do not have to fire a real event to check your integration. In the dashboard, Settings > API Test lets you send a dry-run event and see exactly what would happen.

Enter an event name, a contact email, and an optional JSON properties object, then send. The tool evaluates the event against your active flows and reports:

  • Would it enroll, and in which flow and branch — or the reason it would not (contact not found, no matching flow, contact not mailable, already enrolled, in cooldown).
  • Per-property validation — which properties map to an existing custom field (and would persist) and which are unknown (and would be silently dropped).

A dry-run never enrolls the contact and never sends an email, so it is safe to run against any address, including a real customer. Each test is recorded in the flow's event log tagged as a test event, and is excluded from your health stats. Toggle Show test events on the campaign's Event Log tab to see them.

This is the quickest way to confirm your event name matches a flow trigger and that your properties carry the custom fields your branches depend on.

Custom Events

You can fire any custom event name via the API. Use descriptive, dot-namespaced names to keep things organized:

purchase.completed
onboarding.step_finished
feature.activated
invoice.overdue

Custom events behave identically to built-in events — any flow with a matching trigger name will be activated.

Sending booleans

Send booleans as the strings "true" and "false", not as JSON true/false:

{ "properties": { "hasShareLink": "true", "viewCount": 12 } }

A raw JSON true is stored as "1", and a raw false is stored as an empty value that reads as though the property was never sent. Flow branch conditions compare exact strings, so "1" matches neither a "true" nor a "false" condition — the contact takes the No path, silently, along with everyone else.

Numbers are fine as numbers. It is only booleans that change shape in transit.

Notes

  • A contact can only be enrolled in the same flow once at a time. Firing the same event again while the contact is mid-flow has no effect.
  • The properties object values are available as variables in flow email templates.
  • A property is only persisted if a custom field of that name already exists. Anything else is accepted, returns success, and is dropped — so a branch reading it finds nothing and takes the No path. Create the custom field first, and check the exact spelling: hasShareLink and hasCreatedShareLink are different fields.
  • Enrollment is created synchronously on receipt. The actual flow execution (email sends, node traversal) runs in the background queue — there may be a short delay before emails are sent.