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

# Create Journey

> Create an SMS welcome journey with triggers and steps.

Creates a journey for your account. Start with a draft, then use [Update Journey Status](/api-reference/v2/journeys/update-journey-status) to activate the journey after reviewing its steps.

Declares the `journeys.write` scope. Authenticate with `X-Api-Key`; see [Authentication](/api-reference/authentication).

## API Key Requirements

Creating, duplicating, editing, or activating journeys requires a live key with no send-confirmation requirement, no daily send cap, and no sending-list restriction. These requirements also apply to drafts. Account or key-owner policy can require confirmation even when the key does not.

Restricted keys retain reads, custom-field refresh, and valid stop operations, subject to their scopes. A detail update is a stop operation only when its entire body is `{"status": "draft"}`, `{"status": "paused"}`, or `{"status": "archived"}`. Archive and the dedicated status endpoint also support stopping.

Empty permission lists, `api.full`, exact journey scopes, and matching wildcard permissions follow the existing scope rules. Account and key-owner scope ceilings can restrict access even for full-access keys.

## Body Parameters

<ParamField body="name" type="string" required>
  Required name after trimming whitespace; maximum 255 characters.
</ParamField>

<ParamField body="trigger" type="object">
  Enrollment group with operator AND or OR, conditions, and nested groups. Conditions use conditionType: list, custom\_field, audience, or any. List conditions use integer listIds; audience conditions use audienceId. Omitted or empty triggers become an empty AND group.
</ParamField>

<ParamField body="priority" type="integer">
  Routing priority; higher values win when several journeys match. Default: 0.
</ParamField>

<ParamField body="steps" type="array of objects">
  Ordered step definitions. Each has stepId, stepType, optional name, config, and nextStepId. Types: send\_message, send\_contact\_card, wait, wait\_until, condition, percentage\_split, set\_custom\_field, unsubscribe, double\_optin, exit. See [Welcome Journeys](/guides/journeys/overview) for step configuration.
</ParamField>

<ParamField body="startStepId" type="string">
  Starting step ID. When steps exist and this field is omitted, the first step becomes the start.
</ParamField>

<ParamField body="status" type="string">
  Initial status; defaults to draft. Accepts draft, active, paused, or archived. Creating an active journey requires a nonempty steps array and a start step contained in it, the same checks the status endpoint applies.
</ParamField>

Retrieve eligible lists, creatives, audiences, offers, and double opt-in configurations with [Get Journey Options](/api-reference/v2/journeys/get-journey-options). References must belong to your account. Step routes must resolve and cannot form cycles.

## Example

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.tracklysms.com/api/v2/journeys" \
    -H "X-Api-Key: trk_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{"name":"Welcome","steps":[{"stepId":"done","stepType":"exit"}],"status":"draft"}'
  ```
</RequestExample>

## Response

The `journey` object uses camelCase: `id`, integer `accountId`, `name`, `trigger`, `priority`, `steps`, `startStepId`, `status`, `pauseReason`, `createdAt`, and `updatedAt`. The trigger can be null. Each step contains `stepId`, `name`, `stepType`, `config`, and nullable `nextStepId`. Pause reasons and absent timestamps can be null.

<ResponseExample>
  ```json 201 theme={null}
  {
    "success": true,
    "message": "Journey created successfully",
    "journey": {
      "id": "664f1a2b3c4d5e6f7a8b9c0d",
      "accountId": 101,
      "name": "Welcome",
      "trigger": {
        "operator": "AND",
        "conditions": [],
        "groups": []
      },
      "priority": 0,
      "steps": [
        {
          "stepId": "done",
          "name": "Exit",
          "stepType": "exit",
          "config": {},
          "nextStepId": null
        }
      ],
      "startStepId": "done",
      "status": "draft",
      "pauseReason": null,
      "createdAt": "2026-09-21T10:00:00",
      "updatedAt": "2026-09-21T10:00:00"
    }
  }
  ```
</ResponseExample>

## Errors and Retry

| HTTP | Code                                 | Meaning                                                                                                   |
| ---- | ------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| 400  | May be absent                        | Request body is not a JSON object, or `name` is not a string; read `error`.                               |
| 400  | May be absent                        | Invalid name, trigger, steps, or routing; read `error`.                                                   |
| 400  | May be absent                        | `status` is not draft, active, paused, or archived; read `error`.                                         |
| 400  | May be absent                        | `status` is active and the journey has no steps, or no start step contained in those steps; read `error`. |
| 401  | `invalid_credentials`                | Missing or invalid key.                                                                                   |
| 403  | `insufficient_scope`                 | The key or account policy does not permit this operation.                                                 |
| 403  | `confirmation_required`              | A send-confirmation policy prevents journey authoring.                                                    |
| 403  | `journey_not_allowed_for_capped_key` | The key has a daily send cap.                                                                             |
| 403  | `list_not_allowed_for_key`           | The key has a sending-list restriction.                                                                   |
| 403  | `sandbox_read_only`                  | The key is a sandbox key.                                                                                 |
| 403  | May be absent                        | A step references a partner offer and that product is not enabled for the account; read `error`.          |
| 409  | `journey_write_conflict`             | Another change won; read `journeyId` and reconcile before resubmitting.                                   |
| 409  | `journey_write_outcome_unknown`      | The write may have committed. Inspect `journeyId` before another write.                                   |
| 503  | `send_policy_unavailable`            | The send policy could not be checked or changed during the write.                                         |

Both 409 responses include `journeyId` and `retryable: false`. After `journey_write_outcome_unknown` on a request that included `status`, inspect the journey; if it shows the status you sent, send the same status once more through the status endpoint, which re-applies the holds for that status. A successful response confirms the journey write.

## Idempotency

Send an [`Idempotency-Key`](/api-reference/v2/idempotency) header with create and [duplicate](/api-reference/v2/journeys/duplicate-journey) requests, and reuse the same key when a lost response leaves the outcome ambiguous. The first 201 is cached for 24 hours and replayed verbatim, so the retry returns the journey the first request created instead of creating a second one. A `journey_write_outcome_unknown` 409 is cached the same way, so a retry replays that verdict rather than attempting the write again.

Reusing a key with a different request body returns `409 idempotency_conflict`, and a retry arriving while the first request is still running returns `409 idempotency_in_progress` with a `Retry-After` header. Duplicate binds its key to the source journey path as well, so the same key aimed at another journey conflicts instead of replaying the earlier copy. Ordinary 4xx results release the key for reuse. Without the header the request runs unprotected: inspect your journeys before repeating it after a lost response.
