Skip to Content
Automations

Automations

An automation is a published flow a contact runs through: a welcome series, an onboarding sequence, a win-back. You build it on the canvas in the app, where the steps, the waits and the emails live. The API does the one part that belongs in your code: deciding who enters, and when.

That split is worth understanding before you write against this. The flow is content and timing, which a human edits. The enrollment is an event in your product, which only your backend knows about. So there are two endpoints here, not forty.

trial started -> POST /automations/{id}/enroll -> day3 sends the whole series

The Automation object

{ "id": "aut_9x4v0dz1k2m7", "object": "automation", "name": "Trial onboarding", "status": "active", "trigger": "api", "audience_id": "aud_7k2m9x4v0dz1", "live_version": 3, "sandbox": false, "created_at": "2026-09-01T09:00:00.000Z", "updated_at": "2026-09-08T11:20:00.000Z" }
FieldNotes
statusdraft, active, paused or archived.
triggeraudience_join if the flow also fires on its own as people join the audience, or api if only enrolling starts it.
audience_idThe audience the flow runs over. A contact must already be in it, or be created by the same call.
live_versionThe published version number, or null if it has never been published.
sandboxtrue when the flow will only mail members of your own organization. Re-stamped on every publish, so it follows your plan.

live_version is null on a draft. A draft cannot be enrolled into, because there is nothing published to run.

List automations

GET/v1/automations

Newest first, cursor-paginated. Needs no scope: it names flows, not people.

ParamNotes
statusdraft, active, paused or archived. Archived are hidden unless you ask for them.
limit, afterStandard pagination.

An unrecognised status is 400 invalid_request rather than an empty list, so a typo tells you it was a typo.

curl "https://go.day3.app/api/v1/automations?status=active" \ -H "Authorization: Bearer $DAY3_API_KEY"

Enroll a contact

POST/v1/automations/{automation_id}/enrollscope: automations:enroll

Puts one person into the flow. From that moment day3 sends them every email in it, on the flow’s own schedule, with no further call from you.

FieldTypeRequiredNotes
emailstringYesCanonicalized, meaning trimmed and lowercased, before anything else.
attributesobjectNoFlat string to string. Creates the contact if it is not in the audience yet, and merges into it if it is.
curl -X POST https://go.day3.app/api/v1/automations/aut_9x4v0dz1k2m7/enroll \ -H "Authorization: Bearer $DAY3_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: trial-start-user-8412" \ -d '{ "email": "jane@acme.com", "attributes": { "plan": "trial", "company": "Acme" } }'
{ "object": "enrollment_result", "outcome": "enrolled", "enrollment_id": "aen_4v0dz1k9x2m7" }

attributes decides whether a stranger is created

This is the one design decision in the endpoint, and it is deliberate:

  • With attributes, an unknown address is created in the flow’s audience as subscribed, then enrolled. Same rules as Contacts: values must be strings, unknown keys register themselves as fields, a null deletes a key, and the merge is shallow.
  • Without attributes, an unknown address comes back as not_subscribed. Nothing is created and nothing is sent.

So enrolling can never quietly grow a list by accident. Send attributes, even an empty object, when you mean “create this person if they are new”.

Outcomes

The call returns 200 whenever it was understood. Only enrolled started a run. Every other outcome names the gate that refused, in the same vocabulary the app uses.

outcomeWhat happened
enrolledA run started. enrollment_id is set.
already_enrolledThe flow’s re-entry rule says this person is already in it.
not_subscribedNot in the audience, or not in subscribed status. Unsubscribed, bounced and pending contacts are all refused here.
suppressedThe address is on the account’s suppression list.
entry_filter_no_matchThe flow has an entry filter and this contact does not match it.
sandbox_not_memberThe flow is in sandbox mode and this address is not a member of your organization.
wrong_audienceThe contact exists, but in a different audience than the flow.
automation_not_activeThe published version is unusable. Rare, and worth reporting.

Branch on outcome. Treating a 200 as success is the mistake this shape exists to prevent.

Always send Idempotency-Key. A lifecycle event that fires twice is the normal case, not the exception, and a duplicate enrollment means somebody gets the whole series twice. See Idempotency.

A paused flow still accepts enrollments

Enrolling into a paused automation succeeds, and the person waits at the trigger. Resuming releases them.

That is deliberate. Pausing for an hour to fix a typo must not silently drop every signup in that hour: a late welcome email is recoverable, a missing one is not.

A draft or archived automation is 409 invalid_request instead, because there is no published version to run.

Why this needs a scope

POST /automations/{id}/enroll requires a key with the automations:enroll scope. It is off by default and cannot be added to an existing key. Without it you get 403 insufficient_scope, and the fix is to mint a new key.

Same reasoning as campaigns:send: it puts mail in a stranger’s inbox. An enrollment is a standing promise to send that contact every email in the flow, on the flow’s schedule, with no further call from the key holder. That is a larger commitment than a single send, not a smaller one.

Listing automations is unscoped, because a list of flow names reaches nobody.

Errors

StatusCodeCause
400invalid_emailThe address is not valid.
400invalid_requestAn unknown status filter, or an attributes value that is not a string.
403insufficient_scopeThe key lacks automations:enroll.
404not_foundNo such automation on this account.
409invalid_requestThe automation is a draft or archived. Publish it first.
409email_suppressedCreating the contact was refused, because the address is suppressed.

Note the difference between 409 email_suppressed and the suppressed outcome. The first is the contact write being refused, which can only happen when you sent attributes. The second is a 200 saying the contact exists and was not enrolled. Both mean the same thing about the address.

From an AI editor

The same two operations are exposed over MCP as day3_list_automations and day3_enroll_in_automation, with the same scope rule on the enroll tool.

What is not here

The canvas itself. Creating flows, editing nodes, wait steps, entry filters, re-entry rules and the enrollment browser are all app-only, and an automation’s statistics are read in the app rather than over v1.

If you need one of those from code, it does not exist yet rather than being undocumented.

Last updated on