Skip to main content
POST /api/v1/events is the primary way to drive Mailr. Your CRM or automation stack pushes an event when something happens — a job completes, a deal is won, a contact is created — and Mailr matches it against the automations you’ve bound and, when one fires, enqueues a per-recipient Street View postcard campaign. Required scope: events:write · Rate limit: 120 events / minute / org · Max body: 256 KB

The trigger-binding concept

There is no “connect” flow for the developer API — your key is the credential. The first valid event lazily creates a hidden custom connection for your org. What turns an event into a postcard is a binding you create in the app:
1

Send at least one live event

Live events are sampled — Mailr keeps a rolling sample of ~25 recent events per event_type so your event types show up in the app’s binding picker. Dry-run events write nothing and are not sampled.
2

Create an automation and bind it

In the app, set an automation’s trigger to Custom API and bind it to an event_type and optionally an object_type. A binding with no object type accepts any; a binding that pins one only fires for that type.
3

Events now fire that automation

Each matching event runs the same trigger core the 14 native CRM integrations use: CRM filters → address check → idempotent enqueue → (for contacts) attribution.
matched_automations in the response counts bindings whose event_type/object_type matched — not bindings that actually mailed. Always read outcomes[].reason for what really happened.

The envelope

string
required
Your event name as noun.verb, lowercase letters/digits/underscores, ≤64 chars (e.g. job.completed). Bound in the app as custom.<event_type>.
string
required
One of job, deal, contact. A contact event that carries an address plus an email or phone is also run through indirect attribution.
string
required
Your idempotency key for this event, ≤255 chars. Replaying the same value is absorbed per bound automation.
string (ISO-8601)
When the event happened. Optional. May lead the server clock by at most 24 hours.
boolean
default:"false"
Validate and report outcomes without writing anything. See Dry run.
string
required
Your record’s id in your system, ≤128 chars.
object
A flat map of string → string, at most 100 keys. Values must be strings (numbers/booleans/nulls are rejected, not coerced) of ≤2000 chars. Keys are lowercase snake_case, ≤64 chars, starting with a letter, and must be a reserved key or cf_-prefixed.

Reserved vs custom properties

These keys have defined meaning to Mailr: Every other property MUST be prefixed cf_. This is deliberate: a typo in a reserved key (zipcode, clientemail) is rejected at the edge with a 422 rather than silently producing a wrongly-addressed postcard.
A campaign needs street and at least one of city / zip. Anything less is reported per automation as reason: "no_address" and nothing mails.
The mailing address is composed from street, city, state and zip. The combined value must be at most 500 characters — a longer composed address is rejected with a 422.

Example

202 Accepted

Outcomes

Each candidate binding reports one reason:
reason
A campaign was enqueued (on a dry run, would be enqueued). matched: true.
reason
The automation’s CRM filters excluded this record.
reason
No street plus city/zip.
reason
This dedup_id already enqueued for this automation. When every candidate is a duplicate, the top-level duplicate is true.
reason
mailr_lead_id matched one of your own leads — see below.
reason
The enqueue itself threw. Logged to your webhook log; not silently reported as queued.

Loop guard

If a Mailr-delivered lead lands in your CRM and your automation fires an event straight back at us, you’d start a campaign off an address Mailr itself just sourced — forever. To prevent that, the outbound lead delivery carries a mailr_lead_id. Echo it back in record.properties.mailr_lead_id on any event derived from a Mailr lead:
When Mailr recognizes one of your own lead ids, every candidate reports reason: "loop_guard" and nothing mails. A mailr_lead_id that is not one of your leads is ignored and the event takes the normal path.

Dry run

dry_run: true runs the full pipeline — binding match, CRM filters, address check, dedup lookup — and returns exactly the outcomes a live call would, but writes nothing: no connection upsert, no sample, no enqueue, no log. Use it to validate a binding before mailing. The response echoes dry_run: true so a log reader can tell a rehearsal from a live call.
A dry run still consumes one unit of the 120/min quota — it does the same work as a live call.

Status codes