---
title: "REST API: flows, recipes, forms and shops"
description: Every REST route for flows, runs, recipes, the incoming webhook, signup and connected forms, WooCommerce, Easy Digital Downloads and visitor reports.
url: "https://hiveplugins.com/docs/hive-mail-rest-api-automation-and-commerce/"
updated: "2026-10-28"
category: Hive Mail
---

# REST API: flows, recipes, forms and shops

All routes start with `/wp-json/hivemail/v1`. Sign in as described in [authentication](/docs/hive-mail-rest-api-authentication/); errors and paging are on [the overview](/docs/hive-mail-rest-api-reference/). Capability letters: **V** `hivemail_view_audience`, **A** `hivemail_manage_audience`, **F** `hivemail_manage_automations`, **I** `hivemail_view_intelligence`, **S** `hivemail_manage_settings`. Routes that accept `PUT` also accept `POST` and `PATCH`.

## Flows and runs

All need **F**, except a contact's flows (**V**). A flow is a graph `{ version: 1, nodes[], edges[] }`. A node is `{ id, type, props, position: {x, y}, label? }`; an edge is `{ from, to, port }` (`out` for most steps, `yes`/`no` for a condition, `a`/`b` for a split, `met`/`not_met` for a wait and a goal). States: `draft`, `active`, `paused`. A flow with people in it cannot be deleted (409 `hivemail_flow_in_use`).

| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/flows` | `status`, `search`, `orderby`, `order`, `page`, `per_page` (max 100). Items carry run counts, no graph |
| POST | `/flows` | `name` (required), `description`, `graph`, `settings`. `201`, a draft with `problems[]` and `ready` |
| GET | `/flows/catalogue` | The node types, triggers and fields the canvas is drawn from |
| POST | `/flows/validate` | `graph` in; `{ graph (cleaned), problems[], ready }` out. Saves nothing |
| GET | `/flows/{id}` | The flow with `etag`, `graph`, `problems`, `runs` |
| PUT | `/flows/{id}` | `name`, `description`, `graph`, `settings`, `etag`. A stale `etag` is `409 hivemail_flow_conflict` carrying the saved flow |
| DELETE | `/flows/{id}` | Removes it with its runs and log |
| POST | `/flows/{id}/activate` | `409 hivemail_flow_not_ready` with `data.problems` when something blocks it |
| POST | `/flows/{id}/pause`, `/stop` | Pause (nobody new, nobody moves) or stop (every open run ends; the flow is a draft) |
| POST | `/flows/{id}/duplicate` | `201` |
| POST | `/flows/{id}/enrol` | `{ contact_id }` answers `{ run_id, enrolled }`; `{ audience }` (the campaign audience format) answers `202` with a background job. 409 while the flow is off |
| POST | `/flows/{id}/test` | `contact_id`. The path a dry run takes; nothing is done |
| GET | `/flows/{id}/report` | People, steps reached, email numbers per step, attributed revenue |
| GET | `/flows/{id}/runs` | `status` (`open` or a run state), `node`, `page`, `per_page` |
| GET | `/flows/{id}/runs/{run_id}` | One run with its step log |
| POST | `/flows/{id}/runs/{run_id}/stop` | End one run |
| GET | `/runs` | Every flow's runs: `flow`, `contact`, `status`, `page`, `per_page` |
| GET | `/contacts/{id}/flows` | One contact's runs (**V**) |

```
# Validate a graph: when someone subscribes, add tag 7
curl -u "admin:abcd efgh ijkl mnop qrst uvwx" -X POST -H "Content-Type: application/json" 
  -d '{"graph":{"version":1,"nodes":[
        {"id":"trigger","type":"trigger","props":{"trigger":"subscribed","settings":{}},"position":{"x":0,"y":0}},
        {"id":"tag","type":"tag","props":{"op":"add","id":7},"position":{"x":0,"y":180}}],
       "edges":[{"from":"trigger","to":"tag","port":"out"}]}}' 
  "https://example.com/wp-json/hivemail/v1/flows/validate"
# {"graph":{...},"problems":[],"ready":true}
```

Use the same body with `"name"` on `POST /flows`, then `POST /flows/{id}/activate`. To add everyone on list 1 to an active flow:

```
await fetch( `https://example.com/wp-json/hivemail/v1/flows/${ id }/enrol`, {
    method: 'POST',
    headers: { Authorization: auth, 'Content-Type': 'application/json' },
    body: JSON.stringify( { audience: { lists: [ 1 ] } } ), // 202 { job }; follow it on /jobs/{id}
} );
```

Enrolling twice enrols nobody twice (the second answer is `{ "run_id": 0, "enrolled": false }`); the flow's re-entry setting decides the rest. Deleting a flow that still has people in it is `409 hivemail_flow_in_use`: stop it first.

## Recipes

| Method | Route | Cap | Purpose |
| --- | --- | --- | --- |
| GET | `/recipes` | F | Ready-made flows with their steps in words, what they create, and `available` (false with a `reason` when a plugin they need is missing) |
| POST | `/recipes/{id}/install` | F | `name` optional. `201 { flow, created, reused, placeholders }`. The flow is a draft. 404 `hivemail_recipe_not_found`; 409 `hivemail_recipe_unavailable` |

Install makes only what is missing (lists, tags, fields, segments, email templates by name) and never switches the flow on.

## Incoming webhook trigger

A flow whose trigger is **An incoming webhook arrives** has an address that is not under `/wp-json`. Two REST routes manage it (both **F**):

| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/flows/{id}/webhook` | `{ url, secret, header, enabled }`. Made on first use |
| POST | `/flows/{id}/webhook/rotate` | New address and secret; the old ones stop at once |

Posting to the address, signing the body and the answers are on [Webhooks and API keys](/docs/hive-mail-webhooks-and-api-keys/).

## Signup forms

| Method | Route | Cap | Purpose |
| --- | --- | --- | --- |
| GET | `/forms` | A | List, with `submissions`, `confirmed`, `shortcode` |
| POST | `/forms` | A | `title` and `settings` (at least `list_ids`, the lists people join). `201` with a `Location` header. `400 hivemail_invalid_form` with `errors{ key: message }` |
| GET | `/forms/captcha` | A | `{ provider, label, ready }` (never a key) |
| POST | `/forms/preview` | A | Unsaved `settings` in; `{ html, css_url }` out |
| GET | `/forms/{id}` | A | One form |
| PUT | `/forms/{id}` | A | Send `version` to guard against two people saving at once: a stale one is `409 hivemail_form_conflict` (`data.version` is the current one) |
| DELETE | `/forms/{id}` | A |  |
| POST | `/forms/{id}/duplicate` | A | `201`, an off copy |
| GET | `/forms/{id}/preview` | A | The rendered HTML |
| POST | `/forms/{id}/submit` | public | A signup. Keys `email`, `first_name`, `last_name`, `consent`, `website` (honeypot, leave empty), `t` (the time token), `captcha` |

Submit answers `200 { ok: true, message }` for every valid outcome and for a robot, so the form cannot reveal who is subscribed. Errors are `{ ok: false, code, message }`: 400 (`email`, `name`, `consent`, `captcha`, `expired`), 404 (`closed`), 429 (`limit`: 3 a minute, 10 an hour per visitor). The form needs a signed time token that is 3 seconds to 24 hours old, so build forms with the [shortcode or block](/docs/hive-mail-shortcodes-and-block/) rather than posting by hand.

## Connected forms (other form plugins)

The plugin keys are `fluentforms`, `wpforms`, `cf7`, `gravityforms` and `elementor`.

| Method | Route | Cap | Purpose |
| --- | --- | --- | --- |
| GET | `/integrations` | S | What was found on this site, and how many flows use each trigger |
| GET | `/integrations/{key}/forms` | F | `{ supported, items[{id, title}] }` |
| GET | `/integrations/{key}/forms/{form_id}/fields` | F | The form's fields where the plugin can list them |
| POST, GET, DELETE | `/integrations/{key}/capture` | F | Start, read or stop "show me what the next submission sends" (10 minutes) |
| POST | `/integrations/connect` | F | `key`, `form_id`, `form_title`, `mapping`, `consent_field`, `lists[]`, `tags[]`, `double_optin`. Makes one inactive flow "Form: {title}". 409 if the form is already connected |
| GET | `/integrations/connected` | F | The connected forms |
| DELETE | `/integrations/connected/{flow_id}` | F | Pauses the flow and removes the connection; the flow stays |

## Shops

`{shop}` is `woocommerce` or `edd`. A shop that is not active answers `404 hivemail_shop_missing`; `edd` has no carts (`404 hivemail_shop_no_carts`).

| Method | Route | Cap | Purpose |
| --- | --- | --- | --- |
| GET | `/commerce/{shop}/carts` | V | `status` (`open`, `abandoned`, `recovered`, `ordered`), `search`, `page`, `per_page` (max 100) |
| GET | `/commerce/{shop}/carts/{id}` | V | One cart with `restore_url` |
| DELETE | `/commerce/{shop}/carts/{id}` | A | Forget a cart; its restore link stops working |
| GET | `/commerce/{shop}/customers` | V | `search`, `orderby` (`last_order_at`, `total_spent`, `orders_count`, `first_order_at`, `email`, `name`), `order`, `page`, `per_page` |
| GET | `/commerce/{shop}/index` | V | The customer index summary and its last rebuild |
| POST | `/commerce/{shop}/index/rebuild` | S | `202 { job }`: recompute every customer |
| GET | `/commerce/lookup/{kind}` | V, F or C | `search` or `ids`. Kinds `woo_product`, `woo_category`, `edd_download`, `edd_category` (`404 hivemail_lookup_unknown` for another kind) |

```
curl -u "admin:abcd efgh ijkl mnop qrst uvwx" 
  "https://example.com/wp-json/hivemail/v1/commerce/woocommerce/carts?status=abandoned&per_page=10"
```

## Visitor reports and journeys

All need **I**. `range` is `today`, `7`, `30` or `90` (days, in the site time zone; default 30). With tracking off they still answer `200` with zeros.

| Method | Route | Purpose |
| --- | --- | --- |
| GET | `/intelligence/overview` | Visitors, sessions, pageviews, conversions, revenue, a timeline |
| GET | `/intelligence/live` | The last 5 minutes: visitors now, pages, a feed. Poll every 5 to 10 s |
| GET | `/intelligence/pages` | `range`, `search`, `orderby`, `order`, `page`, `per_page` (max 100) |
| GET | `/intelligence/pages/{id}` | One page: totals, timeline, top sources |
| DELETE | `/intelligence/pages/{id}` | Forget a page and its hits (**S**) |
| GET | `/intelligence/sources` | `range`, `model` (`last`, `first`), `by` (`channel`, `source`, `campaign`) |
| GET | `/intelligence/conversions` | Goals, conversions, revenue by channel |
| GET | `/intelligence/goals` | The goals (up to 20) |
| PUT | `/intelligence/goals` | Replace the goals (**S**) |
| GET | `/intelligence/consent` | The consent tools found on the site (**S**) |
| GET | `/intelligence/visitors` | `range`, `identified` (`all`, `yes`, `no`), `search`, `page`, `per_page` |
| GET | `/intelligence/visitors/{id}` | One visitor's visits |
| GET | `/contacts/{id}/journey` | Needs **I** and **V** |

Money is a list per currency (`[{ currency, amount }]`), never added across currencies. `GET /intelligence/goals` and `GET /integrations/connected` answer a bare list, not an object.

## By name only

The assistant (`/ai/*`, capability `hivemail_use_ai`), Migration from FluentCRM (`/migration/*`), Check my list (`/email-checks`), Roles (`/roles/*`) and API keys (`/api-keys/*`) are registered under the same namespace and follow the same rules. They are described on their own screens' pages.

Next: [Webhooks and API keys](/docs/hive-mail-webhooks-and-api-keys/), [Hooks: actions](/docs/hive-mail-hooks-actions/).
