> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Journeys

> Create and manage customer journeys with visual flow definitions, test mode simulation, and enrollment analytics.

<Note>
  **See also**: [Journeys concept and configuration](/studio/journeys) for what this API powers, when to call it, and how it is configured.
</Note>

<Frame caption="The Journeys editor.">
  <img src="https://mintcdn.com/kaireonai/l-jsUQlUEuA3B6hG/images/screenshots/journeys-list.png?fit=max&auto=format&n=l-jsUQlUEuA3B6hG&q=85&s=2e2ed9d0a70d7e35faa1d20d169b20d6" alt="Journeys list view in the Studio module" width="1440" height="900" data-path="images/screenshots/journeys-list.png" />
</Frame>

## GET /api/v1/journeys

List all journeys with offset-based pagination.

### Query Parameters

| Parameter | Type    | Default | Description                                                       |
| --------- | ------- | ------- | ----------------------------------------------------------------- |
| `status`  | string  | —       | Filter by status: `"draft"`, `"active"`, `"paused"`, `"archived"` |
| `limit`   | integer | `50`    | Max results (max 200)                                             |
| `offset`  | integer | `0`     | Pagination offset                                                 |

### Response

```json theme={null}
{
  "data": [
    {
      "id": "journey_001",
      "name": "New Customer Onboarding",
      "description": "7-day onboarding sequence for new customers",
      "status": "active",
      "maxDurationDays": 30,
      "entryCondition": { "event": "account_created" },
      "definition": { "nodes": [], "edges": [] },
      "createdAt": "2026-02-15T10:00:00.000Z"
    }
  ],
  "pagination": { "total": 5, "limit": 50, "offset": 0, "hasMore": false }
}
```

***

## POST /api/v1/journeys

Create a new journey.

### Request Body

| Field             | Type    | Required | Description                                                                   |
| ----------------- | ------- | -------- | ----------------------------------------------------------------------------- |
| `name`            | string  | Yes      | Journey name (max 255 chars)                                                  |
| `description`     | string  | No       | Description (max 2000 chars)                                                  |
| `definition`      | object  | No       | Flow definition with `nodes` and `edges`. Default: `{ nodes: [], edges: [] }` |
| `entryCondition`  | object  | No       | Entry trigger condition                                                       |
| `maxDurationDays` | integer | No       | Max journey duration in days (1-365). Default: `30`                           |

### Definition validation

The `definition` graph is structurally validated on create and update. A payload that fails any of these checks is rejected with `422 VALIDATION_ERROR` (`details.fields.definition` lists every problem):

* `nodes` and `edges` must be arrays (of objects)
* every node needs a non-empty string `id`, and node ids must be unique
* every edge's `source` and `target` must reference an existing node id
* the graph must be acyclic (the runtime engine has no loop guard, so cycles are rejected at save time)

Node `type` values are not restricted — the engine executes `entry_trigger`, `wait`, `condition_split`, `nba_decision`, `channel_action`, `ab_split`, `webhook_wait`, and `exit` (the Studio editor exposes the first six; `ab_split` and `webhook_wait` are API-authored).

```json theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid journey definition",
    "status": 422,
    "details": { "fields": { "definition": ["edges[0].target \"ghost\" does not reference an existing node"] } }
  }
}
```

### Example

```bash theme={null}
curl -X POST https://playground.kaireonai.com/api/v1/journeys \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -d '{
    "name": "New Customer Onboarding",
    "description": "7-day onboarding sequence",
    "maxDurationDays": 30,
    "entryCondition": { "event": "account_created" }
  }'
```

**Response:** `201 Created`

***

## GET /api/v1/journeys/{id}

Get journey details.

***

## PUT /api/v1/journeys/{id}

Update a journey. All fields are optional.

| Field             | Type    | Description                                                                                            |
| ----------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `name`            | string  | Updated name                                                                                           |
| `description`     | string  | Updated description                                                                                    |
| `status`          | string  | `"draft"`, `"active"`, `"paused"`, `"archived"`                                                        |
| `definition`      | object  | Updated flow definition (structurally validated — see [Definition validation](#definition-validation)) |
| `entryCondition`  | object  | Updated entry condition                                                                                |
| `maxDurationDays` | integer | Updated max duration                                                                                   |

**Response:** `200 OK`

***

## DELETE /api/v1/journeys/{id}

Delete a journey. Requires admin role.

**Response:** `204 No Content`

***

## POST /api/v1/journeys/{id}/enroll

Enroll one or more customers into an active journey. The journey must be in `active` status. Duplicate enrollments are silently skipped (not errors).

### Request Body

| Field         | Type      | Required | Description                                |
| ------------- | --------- | -------- | ------------------------------------------ |
| `customerId`  | string    | No       | Single customer ID to enroll               |
| `customerIds` | string\[] | No       | Batch of customer IDs to enroll (max 1000) |

Provide either `customerId` or `customerIds`, not both.

### Response -- `201 Created`

```json theme={null}
{
  "journeyId": "journey_001",
  "journeyName": "New Customer Onboarding",
  "enrolled": 5,
  "skipped": 1,
  "errors": 0,
  "total": 6
}
```

| Field         | Description                                           |
| ------------- | ----------------------------------------------------- |
| `journeyId`   | The journey ID                                        |
| `journeyName` | The journey's display name                            |
| `enrolled`    | Number of customers newly enrolled                    |
| `skipped`     | Number of customers already enrolled (duplicates)     |
| `errors`      | Number of enrollment failures (e.g., database errors) |
| `total`       | Total customers processed                             |

### Error Codes

| Code  | Reason                            |
| ----- | --------------------------------- |
| `400` | Journey is not in `active` status |
| `404` | Journey not found                 |

***

## POST /api/v1/journeys/{id}/test

**Test Mode — a real-engine dry run.** Runs one (real or synthetic) customer through the whole journey and returns a step-by-step trace. Condition splits use the production operator logic and NBA Decision steps call the **real** decision engine (draft flows previewed), so the offers returned are the ones that customer would actually receive. To keep it safe and instant: **waits are fast-forwarded**, **Channel Action sends are simulated** (nothing is delivered), and **nothing is persisted** (no enrollment, no queue jobs). See [Testing a journey](/studio/journeys#testing-a-journey-dry-run). Requires the `admin` or `editor` role.

### Request Body

| Field                | Type   | Required | Description                                                                                                    |
| -------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------- |
| `customerId`         | string | No       | Loads this customer's real attributes so splits/decisions run against real data                                |
| `customerAttributes` | object | No       | Synthetic attributes, overlaid on top of the real ones (or used alone), e.g. `{ "tier": "gold", "score": 72 }` |

### Response

```json theme={null}
{
  "steps": [
    { "order": 0, "nodeId": "n1", "nodeType": "entry_trigger", "label": "Entry", "detail": "Customer enters the journey.", "timestamp": "2026-03-16T14:30:00.000Z" },
    { "order": 1, "nodeId": "n2", "nodeType": "condition_split", "label": "Condition", "branch": "true", "detail": "Condition met (tier eq \"gold\") → true branch.", "timestamp": "2026-03-16T14:30:00.000Z" },
    { "order": 2, "nodeId": "n3", "nodeType": "nba_decision", "label": "Decision", "detail": "NBA decision (flow: welcome-flow) → Gold Card Upgrade (0.87), Travel Rewards (0.62).", "decision": { "flowKey": "welcome-flow", "offers": [ { "name": "Gold Card Upgrade", "score": 0.87 }, { "name": "Travel Rewards", "score": 0.62 } ] }, "timestamp": "2026-03-16T14:30:00.000Z" },
    { "order": 3, "nodeId": "n4", "nodeType": "channel_action", "label": "Send", "simulated": true, "detail": "Would send \"Welcome Email\" via Email — simulated, nothing delivered.", "timestamp": "2026-03-16T14:30:00.000Z" },
    { "order": 4, "nodeId": "n5", "nodeType": "exit", "label": "Exit", "detail": "Journey complete: completed.", "timestamp": "2026-03-16T14:30:00.000Z" }
  ],
  "outcome": "exited",
  "visitedNodeIds": ["n1", "n2", "n3", "n4", "n5"],
  "notes": [
    "Send steps are simulated — no message was delivered to the customer."
  ]
}
```

| Field            | Type      | Description                                                                                                                                                   |
| ---------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `steps[]`        | array     | Ordered trace. Each step: `order`, `nodeId`, `nodeType`, `label`, `detail`, optional `branch` (splits), `decision` (NBA offers), `simulated` (sends/webhooks) |
| `outcome`        | string    | `exited` \| `completed` \| `ended (no outgoing edge)` \| `ended (cycle detected)` \| `ended (max steps)` \| `empty`                                           |
| `visitedNodeIds` | string\[] | Node ids in visit order — used to highlight the path on the canvas                                                                                            |
| `notes`          | string\[] | Caveats restating which steps were fast-forwarded or simulated                                                                                                |

### Node behavior in Test Mode

| Node Type         | Behavior                                                                           |
| ----------------- | ---------------------------------------------------------------------------------- |
| `entry_trigger`   | Journey entry point                                                                |
| `wait`            | **Fast-forwarded** — recorded, no real delay                                       |
| `condition_split` | Evaluates the real condition (`eq/neq/gt/gte/lt/lte/in/exists`); routes true/false |
| `nba_decision`    | Calls the **real** decision engine and returns the ranked offers                   |
| `channel_action`  | **Simulated** — reports the would-be send; nothing delivered                       |
| `exit`            | Journey complete                                                                   |

***

## POST /api/v1/journeys/callback/{token}

Resumes an enrollment that is paused on a `webhook_wait` step. When the engine reaches a `webhook_wait` node it generates a one-time UUID token, an HMAC-SHA256 signature, and an expiry (default 48h, `timeoutHours` config), stores them on the enrollment, and sets the enrollment status to `waiting`. An external system calls this endpoint to advance the journey.

**This endpoint is public — no session or API key required.** The signed URL *is* the credential: the route verifies the HMAC signature (keyed by `WEBHOOK_SIGNING_SECRET`) and expiry, and the token is one-time use. It is rate-limited per IP (10/min) and per token (5/min).

```bash theme={null}
curl -X POST "https://playground.kaireonai.com/api/v1/journeys/callback/<token>?sig=<hmac>&exp=<iso-expiry>" \
  -H "Content-Type: application/json" \
  -d '{ "event": "external-approval" }'
```

The optional JSON body is passed to the engine as customer context — a `condition_split` immediately after the `webhook_wait` can branch on it.

### Response — `200 OK`

```json theme={null}
{
  "enrollmentId": "b4ab4a02-64ed-4684-a9a3-b61666b66eda",
  "previousStepId": "hook",
  "nextStepId": "final_exit",
  "action": "exit:done",
  "status": "exited"
}
```

### Error Codes

| Code  | Reason                                                                           |
| ----- | -------------------------------------------------------------------------------- |
| `400` | Token is not a UUID, or `sig`/`exp` query parameters are missing                 |
| `403` | HMAC signature verification failed                                               |
| `404` | Token unknown, already used (one-time), or the enrollment is no longer `waiting` |
| `410` | Callback token has expired                                                       |
| `429` | Rate limit exceeded (10/min per IP, 5/min per token)                             |
| `503` | `WEBHOOK_SIGNING_SECRET` is not configured (fail-closed)                         |

***

## GET /api/v1/journeys/{id}/analytics

Get journey funnel analytics including enrollment counts, completion rates, and per-step metrics.

### Response

```json theme={null}
{
  "funnel": {
    "totalEntries": 15200,
    "activeCount": 3400,
    "completedCount": 8900,
    "exitedCount": 2900,
    "completionRate": 0.5855
  },
  "steps": [
    { "nodeId": "n1", "nodeType": "entry_trigger", "reached": 15200, "dropoff": 0 }
  ]
}
```

***

## Roles

| Endpoint                       | Allowed Roles         |
| ------------------------------ | --------------------- |
| `GET /journeys`                | admin, editor         |
| `POST /journeys`               | admin, editor         |
| `PUT /journeys/{id}`           | admin, editor         |
| `DELETE /journeys/{id}`        | admin                 |
| `POST /journeys/{id}/enroll`   | admin, editor         |
| `POST /journeys/{id}/test`     | admin, editor         |
| `GET /journeys/{id}/analytics` | admin, editor, viewer |

See also: [Journeys](/studio/journeys)
