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

> ## Agent Instructions
> For Bahn Customer API V2 integration work, start with /skill.md and /guides/agent-integration.md on this documentation site. Use /llms.txt to find the guides for the requested workflow.
> The main integration orders your company's transports from existing software. Follow the requested screen, service, or job. Omit ordered_for for own-account orders. Use the platform-ordering guide to choose order ownership and billing for businesses using a platform. Add a customer-facing interface only when requested.
> Use /openapi.yaml for paths, schemas, scopes, and operation preconditions. Use the guides for transport rules and recovery behavior. Load only the relevant pages.
> Use the sandbox for development and tests. Keep credentials on the server. Production orders start real transport work; follow the customer's authorized workflow and account context.
> For required writes, preserve write keys and request data across retries, and use current actions and quoted revisions for order changes. When consuming webhooks, verify and durably store them before acknowledgement.
> Deliver the implemented workflow with test evidence, setup steps, and remaining gaps. Distinguish simulated checks from requests actually verified against Bahn.

# Change or cancel an order

> Change or cancel an order safely and recover from timeouts and conflicts.

You can change or cancel an order yourself until the first driver books it. [Order status](/guides/order-status#when-changes-and-cancellation-close) explains when these actions close. After they close, contact Bahn.

## Send a safe write

A change and a cancellation use the same procedure:

<Steps>
  <Step title="Read the current order">
    Call `GET /v2/orders/{order_id}`. Use this response, not a copy that you stored earlier.
  </Step>

  <Step title="Check the action">
    Continue only when `allowed_actions` contains `update` or `cancel`. If it does not, show `cancellation_unavailable_reason` and stop.
  </Step>

  <Step title="Send the current revision">
    Put the order `revision` inside double quotation marks in the `If-Match` header.
  </Step>

  <Step title="Send a new idempotency key">
    Create a new `Idempotency-Key` for this decision. Store the key, the `If-Match` value, and the body before you send the request.
  </Step>

  <Step title="Retry with the same values">
    After a timeout or a `5xx` response, send the same key, `If-Match` value, and body again.
  </Step>

  <Step title="Read again after a conflict">
    After `412 revision_conflict`, read the order again and decide again. Use a new key for the new decision.
  </Step>
</Steps>

The `If-Match` value looks like this:

```http theme={null}
If-Match: "7qS4Jd1cKx9Nw2Yh6mVb8Pz0Rt3Fu5AeLgCiEoUaWQk"
```

A value without quotation marks, a weak value such as `W/"..."`, or `*` returns `400 validation_error`.

An idempotency key has 8 to 255 characters. Keys are shared by all V2 writes for your account, so a cancellation must not use the key of an earlier change. A retry of a completed write does not do the write again. It returns `200` with the current order, which can differ from the first response.

A live ETA change does not change the order revision. A driver booking, an unbooking, and a change of driver do change it.

## Change an order

Send `PATCH /v2/orders/{order_id}` with a JSON Merge Patch body and the content type `application/merge-patch+json`.

| Field | Rule |
| - | - |
| Omitted field | Keeps its current value. |
| `pickup` or `delivery` | Changes only the fields that you supply. |
| `requested_windows` | Replaces the window list of that stop. An empty list clears it. |
| `file_ids` | Adds files. It does not remove files. |
| `customer_reference` or `customer_note` | Send `null` to clear the value. |
| `items` | Contains one item with `role: primary`. It replaces the primary vehicle input and keeps the trade-in unchanged. |
| `service_options` | Needs both `transport_preference` and `inspection`. |

When you clear the pickup windows, keep `available_from` so that pickup still has a start time. When you send `items`, include the VIN or registration number and every vehicle fact that you want to keep. The CMR recipient cannot change after creation.

The new values must follow the same rules as a new order. [Pickup and delivery times](/guides/timing#request-pickup-and-delivery-times) gives the time rules. A change to the route, vehicle, or service can calculate a new price.

This body replaces the requested delivery windows and keeps every other value:

```json theme={null}
{
  "delivery": {
    "requested_windows": [
      {
        "start_at": "2099-08-04T13:00:00+03:00",
        "end_at": "2099-08-04T17:00:00+03:00",
        "timezone": "Europe/Helsinki"
      }
    ]
  }
}
```

This example reads the order and changes its order-level note. Set `BAHN_ORDER_ID` to the order that you intend to change.

```bash theme={null}
ORDER=$(curl --fail-with-body --silent --show-error \
  "https://sandbox.api.bahnexpress.fi/v2/orders/$BAHN_ORDER_ID" \
  --header "Authorization: Bearer $BAHN_ACCESS_TOKEN")
REVISION=$(printf "%s" "$ORDER" | jq -er '.revision')
if printf "%s" "$ORDER" | jq -e '.allowed_actions | index("update") != null' >/dev/null; then
  export BAHN_UPDATE_KEY=$(uuidgen)
  curl --fail-with-body --silent --show-error \
    --request PATCH "https://sandbox.api.bahnexpress.fi/v2/orders/$BAHN_ORDER_ID" \
    --header "Authorization: Bearer $BAHN_ACCESS_TOKEN" \
    --header "Content-Type: application/merge-patch+json" \
    --header "Idempotency-Key: $BAHN_UPDATE_KEY" \
    --header "If-Match: \"$REVISION\"" \
    --data '{"customer_note":"Use the main gate at pickup."}' | jq '{id, revision}'
else
  printf "%s" "$ORDER" | jq -r '.cancellation_unavailable_reason' >&2
fi
```

## Cancel an order

<Warning>
  Cancellation stops open production work. Confirm the customer's intent before you send a production cancellation.
</Warning>

Send `DELETE /v2/orders/{order_id}` with the current revision in `If-Match` and an idempotency key. The request has no body.

This example reads the order and cancels it only when the action is available. Set `BAHN_ORDER_ID` to the intended test order.

```bash theme={null}
ORDER=$(curl --fail-with-body --silent --show-error \
  "https://sandbox.api.bahnexpress.fi/v2/orders/$BAHN_ORDER_ID" \
  --header "Authorization: Bearer $BAHN_ACCESS_TOKEN")
REVISION=$(printf "%s" "$ORDER" | jq -er '.revision')
if printf "%s" "$ORDER" | jq -e '.allowed_actions | index("cancel") != null' >/dev/null; then
  export BAHN_CANCEL_KEY=$(uuidgen)
  curl --fail-with-body --silent --show-error \
    --request DELETE "https://sandbox.api.bahnexpress.fi/v2/orders/$BAHN_ORDER_ID" \
    --header "Authorization: Bearer $BAHN_ACCESS_TOKEN" \
    --header "Idempotency-Key: $BAHN_CANCEL_KEY" \
    --header "If-Match: \"$REVISION\"" | jq '{id, status, allowed_actions, revision}'
else
  printf "%s" "$ORDER" | jq -r '.cancellation_unavailable_reason' >&2
fi
```

These shell examples use `curl`, `jq`, and `uuidgen`. Keep the key when you retry the same request.

A successful cancellation has these results:

* The order status becomes `cancelled`. The order stays available for reads.
* `allowed_actions` contains only `get_files`, and `get_inspection_report` when the order uses the `full` inspection service.
* Location tracking ends.
* Bahn sends `com.bahn.order.cancelled.v1`.
* Bahn withdraws the open transport work for the order. If Bahn offered that work together with other orders, Bahn arranges transport for those orders separately. The cancellation response does not wait for this.

## Handle a rejected write

* `400 validation_error`: A header or the body is not valid. For example, `If-Match` is not inside double quotation marks. Correct the request.
* `403 permission_denied`: The credential does not have the `orders:write` scope. Use a credential with that scope.
* `409 order_not_cancellable`: Cancellation is not available, and `detail` contains the reason. Read the order and show `cancellation_unavailable_reason`. Do not retry.
* `409 order_not_updateable`: Changes are not available. Read the order and show `cancellation_unavailable_reason`. Do not retry.
* `409 idempotency_conflict`: The key belongs to a different request, or the first request with this key is still in progress. Compare with your stored request. If the first request is still in progress, wait and retry the same request.
* `412 revision_conflict`: The order changed after you read it. Read the order again, and decide again with a new key.
* `422 business_rule_violation`: The changed values break a transport rule, and `detail` explains which one. Correct the values. Only a change returns this code.

Bahn checks the action before the revision. When changes and cancellation are closed, you get `409` even if your revision is also out of date.

A driver can book the order while your request is in progress. A cancellation then returns `409 order_not_cancellable`. A change returns `412 revision_conflict`, and the order that you read next no longer contains `update`.

Read [Errors, retries, and conflicts](/guides/errors-and-retries) for authentication, size, and server errors.


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