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

# Integrate a journey

> Configure four moments through API or MCP with dynamic content and independent webhooks

This example evaluates purchase with CSAT, delivery with Woku, use with CES, and recommendation with NPS. Each moment has one instrument. Several aspects of delivery belong in one Woku, rather than separate surveys for each question.

You need Node.js 18 or later, a [Company Key](/docs/en/development/api), uploaded media, and the real secret supplied by the sending system. Management endpoints remain under `/v1`. The moment configuration returns inbound URLs; use those addresses as sender destinations.

## 1. Prepare media and credentials

```bash theme={null}
export WOKU_API_BASE="https://clientapi.woku.app"
export WOKU_API_KEY="<your company key>"
export SENDER_SECRET="<secret supplied by the sending system>"

curl -sS "$WOKU_API_BASE/v1/woku-media" \
  -H "Authorization: Bearer $WOKU_API_KEY" \
  -F "file=@product.webp"
```

The upload returns `fileId`, `filename`, and `type`. Store `fileId` as `WOKU_FILE_ID`. Keep the API key and sender secret in server configuration.

```bash theme={null}
export WOKU_FILE_ID="<fileId returned by the upload>"
export START_MODE="operator"  # Also supports response or webhook.
```

## 2. Create four moments

Save this code as `journey.mjs`. It creates a disabled journey and configures two woku-issued token URLs and two moments using sender signatures. Each moment can have a URL even when it usually advances after a delay.

<Snippet file="journeys/four-moments.mdx" />

Run `node journey.mjs` to create and review the draft. The example disables tickets and plans. In a real program, enable each independently with `recipients.ticketsEnabled` and `recipients.plansEnabled`. Tickets use `ticketEmails`; plans use `planMembers` with company users and `admin` or `assignee` roles. The creator stays as a group administrator.

| Moment         | Content                                                             | Progression                                                   |
| -------------- | ------------------------------------------------------------------- | ------------------------------------------------------------- |
| Purchase       | Shared CSAT with static variables                                   | Enrollment, first response or webhook, depending on entry     |
| Delivery       | Per-evaluation Woku, conditional title, image, folders and trackers | Sender webhook with a secondary 10-day wait                   |
| Use            | Shared CES                                                          | One hour after Delivery, or its webhook if it arrives earlier |
| Recommendation | Per-evaluation NPS with conditional variables                       | Sender webhook                                                |

`contentMode` selects the content source independently of `trigger`. JavaScript is a function body receiving `payload` and returning a string. Imports, network and filesystem access are unavailable. A bounded schema validates the payload before resolving content. Paths cannot contain `constructor`, `prototype` or `__proto__`.

A dynamic Woku image must be a public HTTPS URL. If a secondary wait runs without a webhook, the moment uses its static name, variables and media; that is why the example retains `toolSpec.fileId`. Missing folders are created. Names are optional and fall back to their keys, within folder-name limits.

## 3. Preview and activate

Append these steps to the same file after `sendMoment`. Use an authorized test email address.

```javascript theme={null}
const sample = {
  order: { id: 'order-demo-123', deliveryMode: 'pickup', store: 'store-demo', image: journey.moments[1].toolSpec.imageUrl },
  customer: { email: process.env.TEST_CUSTOMER_EMAIL, tier: 'gold' },
};
if (!sample.customer.email) throw new Error('Set TEST_CUSTOMER_EMAIL');
const preview = await api(`/journeys/${journey.id}/moments/delivery/preview`, { payload: sample });
console.log(preview.preview.title, preview.preview.folder);
await api(`/journeys/${journey.id}`, { enabled: true }, 'PATCH');
```

Preview returns `200`, evaluates JavaScript against the supplied payload, shows composed CSAT/CES/NPS questions where applicable, and never enrolls or sends. Change `deliveryMode` to exercise another branch. Preview does not verify signatures; send a real event after activation to test them.

New v2 moments default to one initial invitation and one next-day reminder. The example declares this explicitly in `sequence`. A zero-day wait means one hour. You can change attempts and windows; existing definitions retain their values.

## 4. Send events with a stable reference

With `START_MODE=operator`, enroll the case before its confirmations:

```javascript theme={null}
await api(`/journeys/${journey.id}/enrollments`, {
  subjectKey: sample.order.id, contact: { email: sample.customer.email },
});
await sendMoment('sale', sample, 'sale-demo-123');
await sendMoment('delivery', sample, 'delivery-demo-123');
await sendMoment('use', sample, 'use-demo-123');
await sendMoment('loyalty', sample, 'loyalty-demo-123');
```

With `START_MODE=webhook`, skip enrollment. The Purchase webhook starts the case. All senders use the same `order.id`; email identifies the contact, while the reference connects that case's events.

Later events may arrive early and wait for their prerequisite. A webhook advances a timed moment and cancels its wait; a late event does not repeat an instrument already sent. Execution exclusion does not rely only on `X-Woku-Event-Id`.

| Authentication          | What the sender receives                                                                         |
| ----------------------- | ------------------------------------------------------------------------------------------------ |
| `url_token`             | The complete woku-issued credential URL; generating another replaces it                          |
| `sender_hmac`           | Its real secret, header, encoding, prefix and signed bytes according to its protocol             |
| Legacy `woku_signature` | The global `webhookSecret` returned on creation or rotation, using the legacy signature protocol |

These credentials are different. Connection reads return a credential-free URL and readiness, never the stored token or sender secret.

## 5. First-response entry

This is an alternative to step 4 enrollment. Run the setup with `START_MODE=response` for a fresh journey or use a new case before sending its events. Customer QR/link entry is also available for operator-mode v2 journeys. Reading or preparing the entry does not start it. Do not prepare another entry for a case already running.

```javascript theme={null}
const entry = await api(`/journey-entries/${journey.id}`, {
  requestId: randomUUID(), reference: sample.order.id, email: sample.customer.email,
});
await api('/csat', {
  csatToolId: entry.toolId, score: 5, clientEmail: sample.customer.email,
  dispatchToken: entry.token,
});
```

The example starts with CSAT. For another first instrument, use its public capture endpoint and pass the token as `dispatchToken`; Woku needs identified text or audio feedback. A valid saved response confirms entry once. Preparation stays pending and does not send another invitation for the first instrument.

## 6. Tracking, retries and stopping

```javascript theme={null}
const page = await api(`/journeys/${journey.id}/enrollments?limit=20`, undefined, 'GET');
console.log(page.items.map(({ id, lifecycle, next }) => ({ id, lifecycle, next })));
// When nextCursor is present, request the next page with ?limit=20&cursor=...
const caseId = page.items[0]?.id;
// To stop only this case: POST /v1/journeys/{id}/enrollments/{caseId}/stop
// with { reason: 'Order cancelled' }.
```

The page returns at most 100 cases, defaulting to 20. `nextCursor` appears only when another page exists. Each case retains its version, history and next action. Editing creates a version for future cases and preserves separately stored credentials; running participations use their earlier snapshot.

A cycle completes on the last instrument's response or 30 days after its first send. Manual enrollment can reuse the key after the prior cycle completes or finishes stopping. Stopping cancels that exact case's waits and reminders, preserves feedback and other cases, and cannot recall delivery already accepted by a provider.

| Result                                                  | Action                                                                                       |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Webhook `200` with `accepted`, `duplicate` or `ignored` | Acknowledge it; an unmatched event may be ignored                                            |
| `400`                                                   | Fix configuration, schema or payload before retrying                                         |
| Webhook `401`                                           | Fix token or signature, including its timestamp                                              |
| API `403`                                               | Check company key and access                                                                 |
| `404`                                                   | Check journey, moment or participation                                                       |
| `409`                                                   | Wait for or resolve the running cycle; do not invent another identity to bypass the conflict |
| `429` or a transient failure                            | Retry with backoff and the same event ID                                                     |

For management POST operations accepting `X-Woku-Idempotency-Key`, use a new key for a new operation and retain its key for retries. Reusing a key with another body replays the original response; using it for a different operation returns `422`.

## The same contract in MCP and SDKs

In MCP, request `woku_guide` with `topic=customer_journeys`. `create_journey` and `update_journey` accept the same advanced moments but call `sequence` `cadence`. Connect them with `preview_journey_moment`, `get_journey_connections`, `mint_journey_moment_url` and `set_journey_sender_secret`. Credential mutations and stopping require explicit confirmation; writes require `mcp:write`.

JavaScript and Python SDKs keep `/v1`, expose generated moment types, and do not activate a journey on creation. See the [MCP reference](/docs/en/mcp/tools) and [journey concepts](/docs/en/core-concepts/journeys).

### Returning from the same QR or link

Keep requestId stable while preparing/retrying the same first tool. If the API
returns HTTP 409 with code `journey_entry_closed`, a completed/stopped cycle was
identified. For a new cycle without an external reference, prepare again with a
new requestId. Never renew the ID just because of a timeout, network error or an
active-evaluation conflict. An explicit purchase reference continues to identify
that same case and must change for a different purchase.
