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

# Run a charging journey

> Preview a journey plan, start it idempotently, follow it live, and resolve a recovery decision when delivery is uncertain.

A conformance suite checks that each endpoint obeys the specification. A **charging
journey** drives a whole story against your partner: a session that starts, meters
energy, completes and produces a CDR, with the tester judging the sequence as well as
each exchange. Journeys run against a registered connection, take minutes rather than
seconds, and stream evidence while they go.

The flow is always the same four steps: browse the catalogue, preview a plan, start the
run, follow it to a verdict.

<Warning>
  **Journeys do not run over a connector.** They execute in a separate worker process that
  cannot reach the socket a connector holds, so the app will not offer a connector-backed
  endpoint for a journey and the API refuses one. Conformance runs are what a connector carries
  today. If you are testing a local service and want journeys, use
  [a tunnel](/guide/connect-a-local-endpoint) instead.
</Warning>

## 1. Browse the catalogue

```bash theme={null}
curl -fsS "https://chargerdojo.com/api/v1/testing/journeys?targetId=$CONNECTION_ID" \
  -H "Authorization: Bearer $DOJO_KEY"
```

Returns the journey packs, scenarios and templates. With `targetId`, the list is filtered
to what that connection can actually run, based on its OCPI version, its role and the
modules it advertises, and the response includes the target's capability summary. Omit
`targetId` to see the full catalogue.

## 2. Preview a plan

A journey is described by a manifest: which template, against which target, on what
clock. Previewing compiles it into an immutable plan and tells you whether it can run,
before anything touches your partner.

```bash theme={null}
curl -fsS -X POST "https://chargerdojo.com/api/v1/testing/journeys/preview" \
  -H "Authorization: Bearer $DOJO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schemaVersion": 1,
    "journey": "first-successful-charge",
    "targetId": "'"$CONNECTION_ID"'",
    "roleUnderTest": "CPO",
    "journeyPack": "core-charging",
    "topology": { "kind": "direct", "profile": "core-ocpi" },
    "ocpiVersion": "2.2.1",
    "clock": { "mode": "accelerated", "durationSeconds": 1800, "accelerationFactor": 60 },
    "session": { "cadenceSeconds": 60, "strategy": "put-and-patch", "updates": ["kwh", "status"] },
    "completion": { "cdrDelaySeconds": 0 },
    "seed": "ci-2026-07-16"
  }'
```

The response carries the compiled plan and the two values the start call needs:

```json theme={null}
{
  "success": true,
  "data": {
    "planId": "...",
    "planHash": "...",
    "expiresInSeconds": 3600,
    "runAllowed": true,
    "plan": {}
  }
}
```

A plan expires after an hour. `runAllowed: false` means the preflight found a reason the
run cannot proceed (an unregistered target, a module the target does not advertise); the
plan spells out which.

## 3. Start the run, idempotently

```bash theme={null}
curl -fsS -X POST "https://chargerdojo.com/api/v1/testing/journey-runs" \
  -H "Authorization: Bearer $DOJO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ci-${CI_PIPELINE_ID}-journey" \
  -d "{\"planId\": \"$PLAN_ID\", \"planHash\": \"$PLAN_HASH\"}"
```

A `202` means the run was admitted and is active work, not a verdict:

```json theme={null}
{
  "success": true,
  "data": {
    "runId": "...",
    "reportId": "...",
    "status": "queued",
    "repeated": false,
    "statusUrl": "/api/v1/testing/journey-runs/...",
    "eventsUrl": "/api/v1/testing/journey-runs/.../events",
    "streamUrl": "/api/v1/testing/journey-runs/.../stream"
  }
}
```

**Always send `Idempotency-Key` from CI.** A repeated start with the same key returns the
original run with `repeated: true` instead of creating a second one, so a retried
pipeline step cannot double-charge your quota or hit your partner twice. The same key
with a different body answers `409`.

The starts that do not admit a run:

| Status | Meaning                                                                                                                                                                                                                           |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402`  | The journey needs a capability your plan does not include, such as the Direct Payment or Booking packs, or more than one session in parallel. The body names the capability. Nothing was admitted and nothing reached the target. |
| `409`  | The plan expired or its hash changed. Preview again and start with the fresh `planId` and `planHash`.                                                                                                                             |
| `429`  | Your plan's run quota is full. Each in-flight journey holds one run slot until it finishes, and a run waiting on a recovery decision keeps holding its slot until you decide. The `Retry-After` header says when to try again.    |
| `503`  | **Our runner is unavailable.** This is a statement about our service, never about your partner. Nothing was admitted and nothing reached the target. Start again later with the same `Idempotency-Key`; the key makes that safe.  |

## 4. Follow it to a verdict

```bash theme={null}
curl -fsS "https://chargerdojo.com/api/v1/testing/journey-runs/$REPORT_ID?wait=55" \
  -H "Authorization: Bearer $DOJO_KEY"
```

`?wait=<seconds>` long-polls until the run reaches a terminal state or the wait expires.
The response carries `status`, the compiled plan, the current stage, the run's verdicts
once they exist, and the two fields the next two sections explain: `runIntegrity` and
`recoveryOptions`.

### What each status means

| Status              | Meaning                                                                                                           |
| ------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `queued`            | Admitted, waiting for a runner.                                                                                   |
| `running`           | The journey is executing against your partner.                                                                    |
| `recovering`        | Our runner restarted and is resuming from its checkpoint. No request is repeated; you do not need to do anything. |
| `recovery_required` | The delivery of one action is uncertain and the run is paused for your decision. See below.                       |
| `completed`         | The journey finished and the verdicts are in. Finished is not the same as passed: read the verdicts.              |
| `failed`            | The journey could not finish.                                                                                     |
| `cancelled`         | You cancelled it.                                                                                                 |
| `interrupted`       | The run ended early with its evidence preserved, for example because you chose to stop it during recovery.        |

### Run integrity is about our evidence, not their conduct

`runIntegrity` states how complete **our** record of the run is:

| Value      | Meaning                                                                                                                                      |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `valid`    | Every action's outcome was observed. The verdicts rest on complete evidence.                                                                 |
| `degraded` | Part of the run is unobservable, for example because you chose to continue past an uncertain action. Verdicts that depend on the gap say so. |
| `invalid`  | The evidence cannot support verdicts.                                                                                                        |

A degraded or invalid run is never a statement that your partner failed. Target failures
show up as failed verdicts with the exchange attached, exactly as in a conformance run.

### Stream the events

Page through events with the sequence number as the cursor:

```bash theme={null}
curl -fsS "https://chargerdojo.com/api/v1/testing/journey-runs/$REPORT_ID/events?after=0&limit=100" \
  -H "Authorization: Bearer $DOJO_KEY"
```

The response's `meta.nextAfter` tells you where to resume; pass it as the next `after`.

Or hold one connection open with server-sent events:

```bash theme={null}
curl -fsSN "https://chargerdojo.com/api/v1/testing/journey-runs/$REPORT_ID/stream" \
  -H "Authorization: Bearer $DOJO_KEY" \
  -H "Accept: text/event-stream"
```

Every SSE message carries its sequence as the event id. If the connection drops, resume
without a gap by reconnecting with the standard `Last-Event-ID` header set to the last id
you saw; the stream replays from there and closes itself when the run reaches a terminal
state.

## Cancel a run

```bash theme={null}
curl -fsS -X POST "https://chargerdojo.com/api/v1/testing/journey-runs/$REPORT_ID/cancel" \
  -H "Authorization: Bearer $DOJO_KEY"
```

Answers `202`: cancellation is a request, and the run winds down at the next safe point
rather than mid-exchange.

## When a run pauses for a recovery decision

Sometimes the runner sends a mutating OCPI request and cannot learn whether it arrived:
the connection died between dispatch and response. Resending it automatically could
duplicate a session or a CDR on your partner's side, so the tester **never replays an
uncertain mutation on its own**. The run pauses as `recovery_required`, holds its slot,
and asks you to choose.

Read the run to see the choices. They are computed by the server for that specific
uncertain action; only what the run read offers is valid:

```bash theme={null}
curl -fsS "https://chargerdojo.com/api/v1/testing/journey-runs/$REPORT_ID" \
  -H "Authorization: Bearer $DOJO_KEY" | jq '{status: .data.status, recoveryVersion: .data.recoveryVersion, options: .data.recoveryOptions}'
```

| Decision                  | What it does                                                                                                                                                |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reconcile`               | Issue a safe read to find out whether the uncertain object reached the target, then continue on complete evidence. Offered when the action has a safe read. |
| `continue_without_replay` | Carry on without resending. The affected stage stays unobservable and the run's integrity becomes `degraded`.                                               |
| `stop`                    | End the run as `interrupted`, preserving all evidence collected so far.                                                                                     |
| `rerun_plan`              | Start a separate, fresh run from the same immutable plan. The paused run keeps its evidence.                                                                |

Apply one with the `recoveryVersion` from the same read:

```bash theme={null}
curl -fsS -X POST "https://chargerdojo.com/api/v1/testing/journey-runs/$REPORT_ID/recovery" \
  -H "Authorization: Bearer $DOJO_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"recoveryVersion\": $RECOVERY_VERSION, \"decision\": \"reconcile\"}"
```

A `409` means your `recoveryVersion` is stale: the run moved on since you read it,
perhaps because a colleague decided first. Re-read the run and decide against the current
state. This is deliberate; two people cannot apply conflicting decisions to the same
uncertain action.

## Put it in CI

A journey grades four things separately: conformance, partner compatibility, your own acceptance
rules, and observations. Gate on conformance, which is the one that says your partner broke a rule;
read the rest on the report.

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

BASE="https://chargerdojo.com"
AUTH="Authorization: Bearer $DOJO_KEY"

# 1. Compile the plan.
PREVIEW=$(curl -fsS -X POST "$BASE/api/v1/testing/journeys/preview" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d @journey-manifest.json)

PLAN_ID=$(printf '%s' "$PREVIEW" | jq -r '.data.planId')
PLAN_HASH=$(printf '%s' "$PREVIEW" | jq -r '.data.planHash')

# 2. Start it. The Idempotency-Key makes a retried job return this same run.
START=$(curl -fsS -X POST "$BASE/api/v1/testing/journey-runs" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: ci-${CI_PIPELINE_ID}-journey" \
  -d "{\"planId\": \"$PLAN_ID\", \"planHash\": \"$PLAN_HASH\"}")

REPORT_ID=$(printf '%s' "$START" | jq -r '.data.reportId')

# 3. Long-poll to a terminal state. Each 200 with a non-terminal status is
#    active work, not an outcome.
while true; do
  # --retry honours the Retry-After header on a 429 and rides out a transient failure, so a
  # quota ceiling reached mid-journey costs you a wait rather than the whole build step;
  # --fail-with-body makes every other non-2xx a non-zero exit with the body still readable.
  RUN=$(curl -sS --fail-with-body --retry 3 \
    "$BASE/api/v1/testing/journey-runs/$REPORT_ID?wait=55" -H "$AUTH") || {
    echo "Could not read the run: $RUN"
    exit 2
  }
  printf '%s' "$RUN" | jq -e '.success' > /dev/null || {
    echo "The API answered something that is not a run: $RUN"
    exit 2
  }
  RUN_STATUS=$(printf '%s' "$RUN" | jq -r '.data.status')
  case "$RUN_STATUS" in
    completed) break ;;
    failed|cancelled|interrupted)
      echo "Journey ended without a verdict: $RUN_STATUS"
      exit 2 ;;
    recovery_required)
      echo "Journey paused for a recovery decision. Decide via the API, then re-run this job."
      exit 2 ;;
    *) sleep 5 ;; # queued, running, recovering: keep waiting
  esac
done

# 4. Gate on the evidence.
INTEGRITY=$(printf '%s' "$RUN" | jq -r '.data.runIntegrity')
if [ "$INTEGRITY" != "valid" ]; then
  echo "Run integrity is $INTEGRITY: our evidence is incomplete. Not a target verdict."
  exit 2
fi

FAILED=$(printf '%s' "$RUN" | jq '.data.verdicts.conformance.failed')

# A finished run always carries counters. If one ever does not, jq hands you `null`, and
# `[ null -gt 0 ]` is a bash error rather than a comparison, which falls through to the
# success path below and prints "compliant". Refuse instead of guessing.
if [ "$FAILED" = "null" ]; then
  echo "The run reported no verdicts to read. Treat this as no answer, not as a pass."
  exit 2
fi

if [ "$FAILED" -gt 0 ]; then
  echo "Charging journey: $FAILED required checks failed"
  exit 1
fi

echo "Charging journey: compliant"
```

Exit `1` is "the target broke a rule". Exit `2` is "we do not have an answer", which
covers our own unavailability, a cancellation, and a pending recovery decision. A build
log should never turn "we do not know" into "your partner is broken".

## Next

[Read your report](/guide/read-your-report), and see
[Troubleshooting](/guide/troubleshooting) for the status codes journeys can answer.

When the catalogue has no journey for the case you are chasing,
[author a custom one](/guide/author-a-scenario): the same JSON the builder produces,
written down so you can version it, diff it and replay it from CI.
