# Flows API

Build automated journeys from code — triggers, waits, branches, and splits as a list of steps.

**Project key.** Acts in the project the key belongs to. An all-access key works too — name the project with an `X-Project-Id` header. See [Authentication](https://emailbump.com/docs/api-keys).

## Endpoints

- `GET /v1/flows`
- `POST /v1/flows`
- `GET /v1/flows/{id}`
- `PATCH /v1/flows/{id}`
- `DELETE /v1/flows/{id}`
- `POST /v1/flows/{id}/activate`
- `POST /v1/flows/{id}/pause`
- `POST /v1/flows/{id}/enroll`
- `GET /v1/flows/{id}/enrollments`

## A flow in one object

A flow is a **trigger** — who enters — and **steps** — what happens to them, in order. Steps run top to bottom until the list ends or the contact exits.

```bash
curl -X POST https://emailbump.com/api/v1/flows \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Welcome series",
    "trigger": { "type": "contact_created" },
    "steps": [
      { "type": "send", "subject": "Welcome to Acme",
        "html": "<p>Hi {{ contact.first_name | default: \"there\" }}, thanks for joining.</p>" },
      { "type": "wait", "days": 3 },
      { "type": "send", "subject": "Three things to try", "html": "<p>Here is where most people start…</p>" }
    ]
  }'
```

A new flow is **active** and enrolling immediately. Pass `"status": "draft"` to build it without starting it.

**Body parameters**

- `name` — *string*, required — Internal name, shown in the dashboard.
- `trigger` — *object*, required — Who enters the flow. See Triggers.
- `steps` — *array*, optional — What happens to them. Required unless the flow is a draft.
- `status` — *string*, optional — `active` (default) or `draft`.
- `reenrollment` — *string*, optional — `not_while_active` (default), `once` ever, or `every_trigger`.
- `entry_filter` — *object*, optional — A condition the contact must also match to enter.
- `goal` — *object*, optional — A condition that removes the contact from the flow when they match it.

`GET /v1/flows/{id}` returns the same shape, `steps` included. A flow drawn on the dashboard canvas can have paths that rejoin, which no step list can express — those come back with `"steps": null` and are edited in the dashboard.

## Triggers

The trigger's fields sit alongside `type`:

- `contact_created` — no fields — every new contact, however they were created.
- `event` — `event_key` — a tracked behaviour, e.g. `{ "type": "event", "event_key": "Purchased" }`.
- `list_join` — `list_id` — joining a list. Omit `list_id` to catch every list.
- `email_event` — `event`, `campaign_id` — `open`, `click`, `delivery`, `bounce`, or `complaint`, optionally on one campaign.
- `segment_enter` — `segment_id` — becoming a member of a saved segment. Evaluated about once a minute.
- `date_based` — `attribute`, `offset_days` — a date attribute coming up, e.g. `renewal_date` with `offset_days: -3`.
- `manual` — no fields — nobody enters on their own; you enroll them.

The trigger says *what* enrolls someone; `reenrollment` says whether it can happen again.

## Steps

Every step is an object with a `type`:

- `send` — `subject` plus exactly one body: `html` (your email, sent as you wrote it — the same contract a campaign's `html` has), `mjml` (compiled first), or `template_id`. Also takes `from_name`, `from_email`, `reply_to`, `preview_text`, and `variants` for an A/B test.
- `wait` — `seconds`, `minutes`, `hours`, `days`, combined freely.
- `wait_until` — `time` (`"09:30"`), optional `weekday` (1 = Monday), optional `timezone`.
- `wait_event` — `event`, a `timeout_*` duration, and `timeout` steps.
- `branch` — `condition`, `yes`, `no`.
- `split` — `branches`, each with `key`, `weight`, and `steps`.
- `add_list` / `remove_list` — `list_id`.
- `set_attribute` — `key`, `value`.
- `notify` — `headline`, `message`.
- `webhook` — `url`, `method`.
- `exit` — no fields.

Personalize subjects and bodies with Liquid. Contacts who are unsubscribed or bounced are skipped at `send` steps automatically.

## A/B test an email

Give a `send` step two or more `variants` and it becomes a test: each contact is assigned one as they reach the step, and the flow report breaks that step down by variation. Every field is an override — leave it out and the base step's value stands.

```json
{
  "type": "send",
  "subject": "Your Acme trial",
  "html": "<p>Here's what to try first…</p>",
  "variants": [
    { "label": "A", "subject": "Your Acme trial starts now", "split_percent": 60 },
    { "label": "B", "subject": "3 things to try in Acme", "split_percent": 40 }
  ]
}
```

- `id` — send back the id `GET` gave you when you edit the flow, so the variation keeps its stats. Minted for you when absent.
- `label` — name for the variation in reporting.
- `subject`, `preview_text`, `html` — overrides.
- `split_percent` — share of contacts, 1–99. Set it on every variation or none (an even split).

Unlike a campaign A/B test there is no winner send: contacts keep arriving for as long as the flow runs, so read the split in the flow report and edit the step when you've seen enough.

## Branching and waiting

A `branch` asks a question about the contact and carries on down `yes` or `no`. Each path is its own list of steps.

```json
{
  "type": "branch",
  "condition": {
    "match": "all",
    "conditions": [
      { "type": "attribute", "key": "plan", "op": "eq", "value": "pro" }
    ]
  },
  "yes": [ { "type": "send", "subject": "Your Pro features", "html": "<p>…</p>" } ],
  "no": [
    { "type": "wait", "days": 2 },
    { "type": "send", "subject": "What Pro adds", "html": "<p>…</p>" }
  ]
}
```

The `condition` is a segment definition — the same shape segments use:

- `property` — `field`, `op`, `value` — `email`, `first_name`, `country`, `city`, `company`, `phone`, `subscribed`, `email_status`, `created_at`. Ops: `eq`, `neq`, `contains`, `is_set`, `is_not_set`.
- `attribute` — `key`, `op`, `value` — a custom attribute. Same ops, plus `gt` / `gte` / `lt` / `lte` for numbers.
- `event` — `key`, `performed`, `within_days`, `min_count`.
- `email` — `event`, `performed`, `within_days`. Add `"scope": "flow_last"` to ask about the last email *this flow* sent them.
- `revenue` — `op`, `amount`, `event_key`, `within_days`.

A `wait_event` step waits for something to happen instead of asking whether it already has. The contact carries on with the following steps the moment the event arrives; if it never does, the `timeout` steps run instead.

```bash
curl -X POST https://emailbump.com/api/v1/flows \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Abandoned checkout",
    "trigger": { "type": "event", "event_key": "Checkout started" },
    "steps": [
      { "type": "wait", "hours": 4 },
      { "type": "wait_event", "event": "Purchased", "timeout_days": 2,
        "timeout": [
          { "type": "send", "subject": "Still thinking it over?", "html": "<p>…</p>" },
          { "type": "wait", "days": 3 },
          { "type": "send", "subject": "10% off, until Friday", "html": "<p>…</p>" }
        ] },
      { "type": "set_attribute", "key": "converted_from", "value": "checkout_flow" },
      { "type": "exit" }
    ]
  }'
```

Paths never rejoin: what follows a `branch`, `split`, or `exit` belongs inside the paths that lead there. Sending steps after one of those returns `400` saying so.

## Activate, pause, delete

A flow is `active`, `paused`, or `draft`. Pausing stops new enrollments; contacts already inside carry on to the end.

```bash
curl -X POST https://emailbump.com/api/v1/flows/FLOW_ID/pause \
  -H "Authorization: Bearer ebk_your_key"

curl -X POST https://emailbump.com/api/v1/flows/FLOW_ID/activate \
  -H "Authorization: Bearer ebk_your_key"
```

`PATCH` edits any of the create fields, `steps` included — a new step list replaces the old one wholesale. Editing a live flow publishes the change immediately; contacts already partway through finish on the version they entered on.

`DELETE /v1/flows/{id}` removes the flow, its steps, and its enrollment history, and returns `204`. Contacts partway through stop where they are, so pause instead if you only want to stop new contacts entering. In the dashboard it's the `⋯` menu on the flow's row under **Automations**.

```bash
curl -X DELETE https://emailbump.com/api/v1/flows/FLOW_ID \
  -H "Authorization: Bearer ebk_your_key"
```

## Enroll and observe

```bash
curl -X POST https://emailbump.com/api/v1/flows/FLOW_ID/enroll \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "email": "jamie@example.com" }'
```

```json
{ "enrolled": true, "flow_id": "c4d5…", "contact_id": "5fef…" }
```

`enrolled: false` means the re-enrollment policy or the entry filter turned them away — not an error.

`GET /v1/flows/{id}/enrollments` shows who is in the flow and where they are, newest first. Filter with `?status=` (`active`, `processing`, `completed`, `exited`, `failed`) and page with `?limit=` and `?offset=`.
