> ## 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.

# Integrar un viaje

> Configura cuatro momentos con API o MCP, contenido dinámico y webhooks independientes

Este ejemplo evalúa compra con CSAT, entrega con Woku, uso con CES y recomendación con NPS. Cada momento tiene una herramienta. La entrega puede evaluar varias cosas en un solo Woku; no se divide en una encuesta por pregunta.

Necesitas Node.js 18 o posterior, una [Clave de Compañía](/docs/development/api), una imagen subida y el secreto real que te entregue el sistema emisor. La API de administración continúa en `/v1`. Las URLs entrantes las devuelve la configuración del momento; usa esas URLs como destinos del emisor.

## 1. Preparar media y credenciales

```bash theme={null}
export WOKU_API_BASE="https://clientapi.woku.app"
export WOKU_API_KEY="<clave de tu empresa>"
export SENDER_SECRET="<secreto que proporciona el sistema emisor>"

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

La subida devuelve `fileId`, `filename` y `type`. Guarda el `fileId` como `WOKU_FILE_ID`. La clave secreta y el secreto del emisor pertenecen a la configuración del servidor.

```bash theme={null}
export WOKU_FILE_ID="<fileId devuelto por la subida>"
export START_MODE="operator"  # También admite response o webhook.
```

## 2. Crear cuatro momentos

Guarda el siguiente código en `journey.mjs`. Crea un viaje apagado y configura dos URLs con token generado por woku y dos momentos con firma del emisor. Puedes generar una URL para cada momento aunque normalmente avance por espera.

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

Ejecuta `node journey.mjs` para crear y revisar el borrador. El ejemplo deja tickets y planes apagados; en un programa real puedes habilitar cada acción por separado con `recipients.ticketsEnabled` y `recipients.plansEnabled`. Tickets usan `ticketEmails`; planes usan `planMembers` con usuarios de la empresa y roles `admin` o `assignee`. El creador permanece como administrador del grupo.

| Momento       | Contenido                                                                 | Avance                                                    |
| ------------- | ------------------------------------------------------------------------- | --------------------------------------------------------- |
| Compra        | CSAT compartido con variables estáticas                                   | Inscripción, primera respuesta o webhook, según el inicio |
| Entrega       | Woku por evaluación, descripción condicional, imagen, carpetas y trackers | Webhook del emisor; espera secundaria de 10 días          |
| Uso           | CES compartido                                                            | Una hora después de Entrega, o su webhook si llega antes  |
| Recomendación | NPS por evaluación con variables condicionales                            | Webhook del emisor                                        |

`contentMode` define de dónde sale el contenido, independientemente de `trigger`. JavaScript es un cuerpo de función que recibe `payload` y devuelve un string. No tiene imports, red ni acceso a archivos. El schema acotado valida el payload antes de ejecutar el contenido. Las rutas no admiten `constructor`, `prototype` ni `__proto__`.

La imagen dinámica de Woku debe ser una URL HTTPS pública. Si configuraste una espera secundaria y el webhook nunca llega, el momento usa su nombre, variables y media estáticos; por eso el ejemplo conserva `toolSpec.fileId`. Las carpetas se crean si no existen. Los nombres son opcionales y usan la clave cuando faltan, respetando los límites de nombres de carpeta.

## 3. Previsualizar y activar

Añade estos pasos al mismo archivo, después de la definición de `sendMoment`. Reemplaza el email por una dirección de prueba autorizada.

```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');
```

El preview devuelve `200`, interpreta el JavaScript con el payload enviado y muestra la pregunta compuesta de CSAT/CES/NPS cuando corresponde y no inscribe ni envía. Cambia `deliveryMode` para probar otra rama. No verifica firmas: compruébalas enviando un evento real después de activar.

En un viaje nuevo v2, cada momento tiene un envío inicial y un recordatorio al día siguiente por defecto. El ejemplo lo declara explícitamente en `sequence`. Una espera de cero días significa una hora. Puedes cambiar los intentos y ventanas; las definiciones anteriores conservan sus valores.

## 4. Enviar eventos con una referencia estable

Si `START_MODE=operator`, inscribe el caso y después emite sus confirmaciones:

```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');
```

Con `START_MODE=webhook`, omite la inscripción. El webhook de Compra inicia el caso. En ambos recorridos, todos los emisores usan el mismo `order.id`; el email identifica al contacto, y la referencia enlaza los eventos del caso.

Un evento posterior puede llegar adelantado y esperar su condición anterior. El webhook adelanta un momento temporal y cancela su espera; un evento tardío no repite una herramienta que ya se envió. La exclusión de ejecución no depende solo de `X-Woku-Event-Id`.

| Autenticación           | Lo que recibe el emisor                                                            |
| ----------------------- | ---------------------------------------------------------------------------------- |
| `url_token`             | La URL completa generada por woku; generar otra reemplaza la anterior              |
| `sender_hmac`           | Su secreto real, header, encoding, prefix y bytes firmados, según su protocolo     |
| `woku_signature` legacy | El `webhookSecret` global de creación o rotación, con el protocolo de firma legacy |

No intercambies estos secretos. Las lecturas de conexión devuelven la URL sin credencial y su estado de configuración, nunca el token guardado ni el secreto del emisor.

## 5. Inicio por primera respuesta

Esta es una alternativa a la inscripción del paso 4. Ejecuta la configuración con `START_MODE=response` para un viaje nuevo o usa un caso nuevo antes de enviar sus eventos. El acceso por QR o enlace también está disponible en viajes v2 con inicio operator. Obtener o preparar la entrada no inicia el recorrido. No prepares otra entrada para un caso que ya está en curso.

```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,
});
```

Este ejemplo tiene CSAT primero. Para otra primera herramienta, usa su ruta pública de captura y pasa el token como `dispatchToken`; en Woku usa un comentario o audio identificado. Guarda la respuesta válida primero; esa respuesta confirma el inicio una sola vez. La entrada preparada aparece pendiente y no manda una nueva invitación a la primera herramienta.

## 6. Seguimiento, reintentos y detención

```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' }.
```

La página devuelve hasta 100 casos, 20 por defecto. `nextCursor` aparece solo si hay otra página. Cada caso conserva su versión, historial y próximo paso. Editar el viaje crea una versión para casos futuros y conserva las credenciales separadas; una participación en curso usa su snapshot anterior.

El ciclo se completa al responder la última herramienta o 30 días desde su primer envío. Una inscripción manual puede reutilizar la misma clave cuando el ciclo previo haya completado o terminado su detención. Detener cancela esperas y recordatorios del caso exacto, conserva feedback y otros casos, y no retira entregas ya aceptadas por un proveedor.

| Resultado                                                 | Qué hacer                                                                                 |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `200` con `accepted`, `duplicate` o `ignored` del webhook | Darlo por atendido; un evento no coincidente puede ser ignorado                           |
| `400`                                                     | Corregir configuración, schema o datos antes de reintentar                                |
| `401` del webhook                                         | Corregir token o firma, incluido su timestamp                                             |
| `403` del API                                             | Revisar la clave, empresa y acceso                                                        |
| `404`                                                     | Revisar viaje, momento o participación                                                    |
| `409`                                                     | Esperar o resolver el ciclo en curso; no inventar otra identidad para evitar el conflicto |
| `429` o fallo transitorio                                 | Reintentar con backoff y el mismo ID de evento                                            |

En POST de administración que admiten `X-Woku-Idempotency-Key`, usa una clave nueva para una operación nueva y conserva la anterior para sus reintentos. Reutilizarla con otro cuerpo reproduce la respuesta original; usarla en otra operación devuelve `422`.

## El mismo contrato en MCP y SDKs

En MCP, pide `woku_guide` con `topic=customer_journeys`. `create_journey` y `update_journey` usan los mismos momentos avanzados, pero llaman `cadence` a `sequence`. Usa `preview_journey_moment`, `get_journey_connections`, `mint_journey_moment_url` y `set_journey_sender_secret` para conectar. Las mutaciones de credenciales y la detención requieren confirmación explícita; las escrituras necesitan `mcp:write`.

Los SDKs JavaScript y Python conservan la API `/v1`, tienen tipos generados de los momentos y no activan un viaje al crearlo. [Consulta la referencia MCP](/docs/mcp/tools) y los [conceptos del viaje](/docs/core-concepts/journeys).

### Volver desde el mismo QR o enlace

Conserva requestId mientras preparas o reintentas la misma primera herramienta.
Si la API devuelve HTTP 409 con código `journey_entry_closed`, identificó un ciclo
completado o detenido. Para un nuevo ciclo sin referencia externa, vuelve a preparar
con un requestId nuevo. No renueves el ID por timeout, error de red ni conflicto
con una evaluación activa. Una referencia de compra explícita sigue identificando
ese mismo caso y debe cambiar para una compra distinta.
