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

# Author a custom journey

> The custom journey JSON shape, the knobs that matter, and how to import, run, and save a journey you wrote by hand or generated from CI.

A **custom journey** is a charging journey described as JSON: the same thing the
Charging Journeys builder produces, written down so a human or an agent can version
it, diff it, and replay it from CI. Import one and it opens ready to run: a
template-shaped file opens with every knob restored, and a file with its own
hand-authored action list opens with that list shown, its values editable in the
builder (more on the difference below). Either way it streams evidence and lands a
report exactly like a catalogue journey.

The fastest way to get a valid file is **Download sample** on the Charging Journeys
page. It exports a ready-to-run custom journey for your selected target. Edit that, or
write one from the shape below.

## The shape

```json theme={null}
{
  "schemaVersion": 1,
  "journey": "first-successful-charge",
  "roleUnderTest": "EMSP",
  "journeyPack": "core-charging",
  "ocpiVersion": "2.2.1",
  "coverageDepth": "happy-path",
  "clock": { "mode": "accelerated", "durationSeconds": 3000 },
  "session": {
    "cadenceSeconds": 300,
    "jitterSeconds": 0,
    "strategy": "put-and-patch",
    "updates": ["kwh", "total_cost", "status", "charging_periods"]
  },
  "catalogue": { "locationUpdates": true, "tariffUpdates": true },
  "completion": { "cdrDelaySeconds": 300 },
  "disturbances": [],
  "seed": "204"
}
```

## The knobs that matter

* **`journey`**: the template the custom journey starts from (e.g. `first-successful-charge`).
  It selects the story; an unknown id falls back to the recommended journey.
* **`ocpiVersion`** and **`targetId`** are advisory in the file. On import the builder
  runs the journey against whichever target you have selected, so a sample from one
  target still runs against another, and you do not need a real `targetId` to share a
  file.
* **`clock.mode`**: `accelerated` compresses a session into seconds; `real` runs it at
  wall-clock cadence for long-session testing.
* **`session.cadenceSeconds`** and **`jitterSeconds`**: how often meter updates are
  pushed, and how irregular they are (`jitterSeconds` above 0 means irregular).
* **`session.updates`**: which fields each update carries (`kwh`, `total_cost`,
  `status`, `charging_periods`).
* **`disturbances`**: faults injected mid-journey to prove the endpoint fails safely.
  The full set is under [Disturbances](#disturbances) below.
* **`coverageDepth`**: `happy-path`, `edge-cases`, or `adversarial`.
* **`seed`**: makes a run reproducible; keep it stable to replay the same story.

Unknown or malformed fields fall back to sensible defaults, so a partial file still
imports and uses the defaults for anything you left out.

### How do I write my own timeline?

There is one exception to that. A file that spells out its own timed `actions` list
carries those **hand-authored actions** into the builder, as the timeline you wrote
rather than the knob editor above.

The Configure step then shows each action's time, kind, and values, in file order. Click
any value to change it in place. Where an OCPI version defines the field, the builder
names the spec file it comes from.

Timing comes from the file. You can still change the endpoint, the clock, and the seed,
and the run streams and grades exactly like any other custom journey. To change an
action's kind, its time, or the order of the list, edit the JSON and import it again.

One thing to watch: the run executes each action at its own `atSeconds`, not in list
order. Keep the list chronological if you want to read it the way it will run.

A file that says `journey: 'custom'` but lists no actions is refused with a message,
rather than imported and quietly emptied. Asking for a custom journey is not something
the importer should discard on your behalf.

## Journeys, packs and what grades them

Three ids in the file name what you are running rather than what it does.

### What can I put in `journey`?

`journey` picks the story to start from. An unknown id falls back to the recommended
journey rather than failing.

`first-successful-charge`, `rough-session`, `charger-reset`, `partner-connection`,
`catalogue-trust`, `remote-start-stop`, `live-price-parking`, `reservation`, `cdr-desk`,
`offline-catch-up`, `hub-routed-charge`, `book-arrive-charge`, `direct-terminal-payment`,
`smart-charging-control`, `hub-connection-status`, `invalid-charging-data`,
`roaming-hub-relay`, and `custom` for a timeline you write yourself.

`roaming-hub-relay` tests a roaming hub through two of your test parties at it, so it is the
one journey that names two targets: `targetId` is the CPO side of the pair and `pairedTargetId`
the eMSP side.

### What does `journeyPack` group?

`journeyPack` groups journeys by the part of OCPI they exercise: `core-charging`,
`foundation`, `catalogue`, `commands`, `billing`, `recovery`, `hub`, `booking`,
`direct-payment`, `charging-profiles`, `hub-client-info`, `negative`, `roaming-hub`.

### What does `oracleIds` do?

`oracleIds` on an action names the checks that judge it. An oracle is the thing that
decides whether what came back was right, so this is how a hand-authored action earns a
verdict instead of merely happening:

`ocpi_response_success`, `session_identity`, `session_progress`, `session_completed`,
`cdr_identity`, `session_cdr_reconciliation`, `catalogue_identity`, `command_response`,
`command_result`, `billing_reconciliation`, `pull_window`, `hub_routing`,
`booking_lifecycle`, `payment_reconciliation`, `charging_profile_result`,
`hub_client_status`.

An action may carry up to 12, and the field is optional: leave it out and the action runs
without a check of its own.

## The actions you can write

An action is one thing the runner does at one moment. Every entry needs three things: what
to do, when, and which scenarios it provides evidence for.

```json theme={null}
{ "action": "session_patch", "atSeconds": 300, "scenarioIds": ["S01"] }
```

### Why is `scenarioIds` required?

`scenarioIds` is required on every action, between 1 and 12 of them, each a capital letter
followed by two digits (`S01`, `G13`). They are what tie an action to the scenarios your report is
graded against: an action with no scenario would run and prove nothing, so the schema does
not accept one. A file missing them is refused on import rather than run.

`atSeconds` counts from the start of the run, up to 48 hours. Actions execute at their own
time, not in list order, so keep the list chronological if you want to read it the way it
will run.

Most kinds are named `module_verb` and send that request to that module, so `tariff_put`
puts a Tariff and `session_get` reads a Session back. Once you know the pattern you can
mostly guess the name you want:

**The handshake and discovery**
`versions_get`, `version_details_get`, `credentials_get`

**A charging session, start to finish**
`prepare_token`, `session_put`, `session_patch`, `session_complete`, `session_get`

**The catalogue your partner reads**
`location_get`, `location_put`, `location_patch`, `location_serve`, `tariff_get`,
`tariff_put`, `tariff_delete`

`location_serve` breaks the naming pattern on purpose: it sends nothing. It changes the Location we
serve your partner, for a partner with no Locations Receiver that has to pull. See
[Push or serve?](#push-or-serve) below.

**Billing**
`cdr_post`, `cdr_get`

**Remote control, and the callback it answers with**
`command_post`, `await_callback`, `charging_profile_get`, `charging_profile_put`,
`charging_profile_delete`

**Bookings** (OCPI 2.3.0)
`booking_get`, `booking_put`, `booking_patch`, `booking_post`

**Direct payment** (the Payments extension)
`payment_get`, `payment_post`, `payment_put`, `payment_patch`

**Hubs**
`hub_client_info_get`, `hub_client_info_put`

**Waiting for your system to call us**
`expect_inbound`, `await_inbound`

**Timing and transport**
`wait`, `transport_cut`, `transport_restore`, `transport_latency`, `sandbox_ptp_step`

Some of these change state on your side and some only read, which is worth knowing before
you point a list at a live endpoint: a `put` or a `post` writes a real object there.

## Disturbances

A disturbance is a fault the runner injects on purpose, to see whether your endpoint
survives something real rather than only the happy path. Adversity is opt-in: a journey
runs clean unless you ask for it.

```json theme={null}
"disturbances": ["skip_intermediate_update", "non_monotonic_meter"]
```

The full set, by what each one disrupts:

* **Session updates**: `skip_intermediate_update`, `duplicate_full_put`,
  `zero_energy_pause`, `non_monotonic_meter`, `patch_before_put`, `pending_start`,
  `session_never_closes`, `session_invalid_close`, `reboot_energy_gap`,
  `offline_replay_burst`
* **Charge records**: `cdr_before_terminal_session`, `delay_cdr`, `early_cdr`,
  `duplicate_cdr`, `credit_cdr`, `corrected_cdr`
* **Pricing and parking**: `tariff_boundary`, `parking_period`
* **Callbacks**: `callback_timeout`, `callback_duplicate`, `callback_out_of_order`
* **Transport**: `transport_outage`

Only these journeys take disturbances: `rough-session`, `charger-reset`, `cdr-desk`,
`offline-catch-up` and `remote-start-stop`. Any other journey refuses a file that lists
one. In the builder you do not pick them one by one: `rough-session` starts with its own
set, and on `charger-reset` the failure variant you choose picks them for you.

Each of those journeys uses only some of the set:

* `rough-session`: `skip_intermediate_update`, `duplicate_full_put`, `zero_energy_pause`,
  `non_monotonic_meter`, `cdr_before_terminal_session`. An empty list turns all five on.
* `charger-reset`: `session_never_closes`, `reboot_energy_gap`, `session_invalid_close`,
  `offline_replay_burst`, `pending_start`, `duplicate_cdr`. It refuses any other.
* `remote-start-stop`: `callback_timeout`.
* `cdr-desk` and `offline-catch-up`: none.

A disturbance one of these journeys accepts but does not use still runs, without that
fault. The preview, the run you start and the builder all name it: the plan and the start
response carry `ignoredDisturbances`, with the ids and a sentence saying the run goes ahead
without them.

To deliver the CDR late, raise `completion.cdrDelaySeconds`, the seconds between the
Session closing and the CDR being sent. `delay_cdr` changes nothing: the journeys above
run without it, and `charger-reset` refuses it. No verdict fails a CDR for arriving late: OCPI lets partners agree to send CDRs
as rarely as once a month, so a receiver that accepts a late one has broken no rule.

An id the runner does not know is refused on import rather than ignored, so a typo tells
you at once.

## The values an action carries

Each hand-authored action can carry a bag of named values, in the file or added in the
builder:

```json theme={null}
{
  "action": "location_patch",
  "atSeconds": 0,
  "scenarioIds": ["G13"],
  "values": { "status": "CHARGING", "ocpp_status": "Preparing" }
}
```

On the Configure step every value is a control. Click one to edit it in place, or use
**+ Add a value** on the action to add a name the file did not carry. Enter saves,
Escape puts it back.

### What can a value be?

A value is text, a number, `true`, `false`, or `null`. A list or an object is refused
in the builder, because a structure belongs in the JSON file rather than in a
single-line editor. Types survive the round trip: `16` comes back as the number your
file held, not as the string `"16"`.

An action can hold up to 40 values, and a name can be up to 64 characters.

Six names belong to the server and an action cannot set them: `url`, `headers`,
`authorization`, `token`, `template`, and `expression`. These carry the request itself,
so a file that could overwrite them could point a run at somewhere you did not
choose.

### Which keys does the runner check?

Most keys are yours to name. A few are read by the runner, which means it also checks
them. Being on this list is not the same as being in the specification, and the two are
marked separately below because they are different kinds of thing:

| Action | Key | Defined by | Rule |
| - | - | - | - |
| `location_patch` | `status` | OCPI, Locations module | Text. Required, because the action will not run without it. |
| `location_patch` | `ocpp_status` | Nobody. Partner extension | Text. Optional. |
| `location_serve` | `status` | OCPI, Locations module | Required, and one of the OCPI EVSE Status values, because we serve it as an OCPI EVSE. |
| `location_serve` | `ocpp_status` | Nobody. Partner extension | Text. Optional. |
| `cdr_post` | `total_parking_time` | OCPI, CDRs module | A number of hours, no longer than the record covers. |

`ocpp_status` is the case worth understanding. No OCPI version names it, and we do not
pretend otherwise: the editor labels it a partner extension exactly as it labels a name
you invent yourself. The runner reads it only so that a value you did supply is checked
rather than silently shipped empty, and so a PATCH, or the Location we serve, can carry the
CSMS status your platform reports alongside the OCPI one. Nothing about it being on this list
makes it standard.

Where a key is one of these, the editor says so while you type, and an empty value is
refused rather than saved into a run that Preview would reject.

### Push or serve?

It depends on whether your partner advertises a Locations Receiver, not on its role.

* **It has a Locations Receiver.** Use `location_patch` to PATCH each state to it, and
  `location_get` straight after to read the EVSE back. The read-back shows whether it kept the
  value it last accepted: a receiver still holding `Preparing` after it took `Finishing` shows a
  driver the wrong state. This is the standard OCPI push to an eMSP, and it is also how a
  Direct Payment terminal provider works: it pulls its Locations first, then takes a `PATCH evse`
  on every status change. Our sandbox keeps `ocpp_status` when you push it, so you can run the
  push against the sandbox first and see the read-back pass before you point it at a partner.
* **It has none, so it can only pull.** Use `location_serve`: it changes the `EVSE-JOURNEY` we
  serve at `LOC-JOURNEY`, moves both `last_updated` values so a delta pull sees the change, and
  records what your system pulled while that state was current. This is the shape for a 2.3.0
  terminal provider, whose Payments module has it "pull the current connector information". It
  runs on OCPI 2.2.1 and 2.3.0. If your terminal only pulls Locations assigned to it, assign
  `LOC-JOURNEY` first.

Pick the clock to match. A push lands the moment it is sent, so accelerated time is fine. A
partner that pulls reads on its own schedule, so a served state needs to last longer than its
polling interval, and real time is the honest choice.

The report grades a served state as an observation, never a failure: if your system did not pull
while `Finishing` was being served, it says so and warns, because nothing in OCPI says when a
partner must pull.

The Choose page's custom journey card downloads a ready parking-fee journey for the endpoint you
have selected: the pushed version if it advertises a Locations Receiver, the served one if not.

### Can I send a field OCPI does not define?

Any other name is yours. The editor labels it a **partner extension** and tells you it
goes on the wire exactly as you write it, so you can model something your platform sends
that the specification never named.

This is deliberate on our side and allowed on the receiving side. OCPI 2.3.0 puts it
plainly: "An OCPI Platform SHALL NOT reject request or response payloads based on the
presence of JSON object field names that are not documented in this specification." A
partner who refuses your extra field is the one out of line, and a run that sends one is
a fair way to find that out before production does.

Two things worth knowing. An extension is not graded, because there is no clause to grade
it against. And a key the runner does not read may be left empty, where a key it does
read may not.

## Import, run, and save

1. On **Charging Journeys**, choose **Import scenario JSON** and pick your file, or
   **Download sample** to start from a working one.
2. The custom journey opens in the builder on the **Configure** step with your knobs
   applied. Adjust anything, then **Preview** to compile the plan.
3. **Start the run.** It streams live and lands a graded report in **Runs**, stored on
   your account, replayable and runnable from CI with the same JSON.

You can annotate a working copy with `//` comments and trailing commas; the importer
strips them. The API and CI expect strict JSON.

See also [Run a charging journey](/guide/run-a-charging-journey) for the run, follow,
and recovery flow.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.