> ## Documentation Index
> Fetch the complete documentation index at: https://evsim.synergyboat.com/docs/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.

## 1. Browse the catalogue

```bash theme={null}
curl -fsS "https://evsim.synergyboat.com/api/v1/testing/journeys?targetId=$CONNECTION_ID" \
  -H "Authorization: Bearer $EVRT_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://evsim.synergyboat.com/api/v1/testing/journeys/preview" \
  -H "Authorization: Bearer $EVRT_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://evsim.synergyboat.com/api/v1/testing/journey-runs" \
  -H "Authorization: Bearer $EVRT_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                                                                                                                                                                                                                          |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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://evsim.synergyboat.com/api/v1/testing/journey-runs/$REPORT_ID?wait=60" \
  -H "Authorization: Bearer $EVRT_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://evsim.synergyboat.com/api/v1/testing/journey-runs/$REPORT_ID/events?after=0&limit=100" \
  -H "Authorization: Bearer $EVRT_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://evsim.synergyboat.com/api/v1/testing/journey-runs/$REPORT_ID/stream" \
  -H "Authorization: Bearer $EVRT_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://evsim.synergyboat.com/api/v1/testing/journey-runs/$REPORT_ID/cancel" \
  -H "Authorization: Bearer $EVRT_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://evsim.synergyboat.com/api/v1/testing/journey-runs/$REPORT_ID" \
  -H "Authorization: Bearer $EVRT_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://evsim.synergyboat.com/api/v1/testing/journey-runs/$REPORT_ID/recovery" \
  -H "Authorization: Bearer $EVRT_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

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

BASE="https://evsim.synergyboat.com"
AUTH="Authorization: Bearer $EVRT_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
  RUN=$(curl -fsS "$BASE/api/v1/testing/journey-runs/$REPORT_ID?wait=60" -H "$AUTH")
  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 ;;
    *) ;; # 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')
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](/docs/guide/read-your-report), and see
[Troubleshooting](/docs/guide/troubleshooting) for the status codes journeys can answer.
