# Writes

> Check guests in, send payment links, cancel and reschedule.

Source: https://www.panion.travel/docs/api/reference/writes

Writes act on real bookings. Each has its own scope, off on a new key unless ticked. There are no bulk endpoints: one booking per request. No write can create a booking or take an amount: a cancel always refunds in full through the same path as Oracle, and a reschedule never changes the price.

Every write needs an `Idempotency-Key` header (missing: 400). Use a new UUID per action and reuse it only to retry that action. For 24 hours the same key with the same request replays the first response with `Idempotent-Replayed: true`; the same key with a different request answers 422; while the first request is still running a duplicate answers 409.

### POST /api/v1/manifest/{id}/checkin

Check a manifest booking in, mark a no-show, or clear it.

Sets the check-in state of one manifest row. `checked_in` with `present` records a partial party (3 of 4). `clear` returns the row to not seen. Only the check-in is stored: the booking, its guests and its money are never changed and your booking system is not told.

Scope: `checkins:write`.
Needs an `Idempotency-Key` header. Counts against the key's daily checkins limit.

Path parameters:

- `id` (string): Manifest row id: the `id` of a passenger in GET /api/v1/manifest.

Request body (JSON):

- `status` (one of `checked_in`, `no_show`, `clear`): `checked_in`, `no_show`, or `clear` (back to not seen).
- `present` (integer, optional): With `checked_in` only: how many of the party turned up. Omit for everyone.

Response fields (200):

- `id` (string)
- `check_in` (object)
  - `status` (one of `checked_in`, `no_show`, nullable)
  - `present` (integer, nullable)

Example: Three of four turned up

```sh
curl -X POST "https://www.panion.travel/api/v1/manifest/5c4b3a29-1807-4f6e-9d5c-4b3a29180716/checkin" \
  -H "Authorization: Bearer $PANION_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"status":"checked_in","present":3}'
```

```json
{
  "id": "5c4b3a29-1807-4f6e-9d5c-4b3a29180716",
  "check_in": {
    "status": "checked_in",
    "present": 3
  }
}
```

Responses:

- 200: Done
- 400: Invalid body, or the Idempotency-Key header is missing
- 401: Missing, invalid, expired or revoked key
- 403: The key lacks the scope
- 404: Not found, or not yours
- 409: A request with this Idempotency-Key is still running
- 422: Not possible for this booking, or the Idempotency-Key was used for a different request
- 429: Rate limited, or the key's daily limit for this action is reached (checkins). Retry-After says when to try again.
- 503: A system this action depends on did not answer; retry with the same key

### POST /api/v1/bookings/{id}/payment-link

Email the guest a link to pay the balance still owed.

Sends a balance payment link for an existing booking: the pay-later balance of a Panion booking, or the balance your booking system says is due on a direct booking. Panion decides the amount; the request takes none. A cancelled booking (which would need a rebook link, and so a new booking) and anything with nothing owed are refused with 422. Calling it again resends the same open link.

Scope: `payment_links:write`.
Needs an `Idempotency-Key` header. Counts against the key's daily payment_links limit.

Path parameters:

- `id` (string): A booking id from GET /api/v1/bookings, or a manifest row id from GET /api/v1/manifest for a booking that exists only in your booking system. Both are UUIDs and never collide.

Request body (JSON):

- (no fields)

Response fields (200):

- `id` (string): The id you sent.
- `kind` (one of `deferred_balance`, `provider_balance`): `deferred_balance`: the balance of a Panion pay-later booking. `provider_balance`: the balance your booking system says is due.
- `url` (string): The payment page. Safe to share with the guest only.
- `amount_minor` (integer)
- `currency` (string)
- `emailed` (boolean): False when the link exists but the email did not go out; share the url another way.

Example: The pay-later balance

```sh
curl -X POST "https://www.panion.travel/api/v1/bookings/8f6d2c1e-4b7a-4e3f-9c2d-1a5b6c7d8e9f/payment-link" \
  -H "Authorization: Bearer $PANION_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "id": "8f6d2c1e-4b7a-4e3f-9c2d-1a5b6c7d8e9f",
  "kind": "deferred_balance",
  "url": "https://www.panion.travel/pay/abc123",
  "amount_minor": 179900,
  "currency": "NOK",
  "emailed": true
}
```

Responses:

- 200: Done
- 400: Invalid body, or the Idempotency-Key header is missing
- 401: Missing, invalid, expired or revoked key
- 403: The key lacks the scope
- 404: Not found, or not yours
- 409: A request with this Idempotency-Key is still running
- 422: Not possible for this booking, or the Idempotency-Key was used for a different request
- 429: Rate limited, or the key's daily limit for this action is reached (payment_links). Retry-After says when to try again.
- 503: A system this action depends on did not answer; retry with the same key

### POST /api/v1/bookings/{id}/cancel

Cancel a booking with a full refund.

Cancels a live (CONFIRMED or RESERVED) Panion booking exactly as Cancel and refund in Oracle: the seats are released in your booking system first, then the guest gets a full refund (the reservation fee on an unpaid pay-later booking), the booking is marked cancelled, the commission is voided, and the guest and your team are emailed. No free-cancellation window or departure check applies: your call is the refund decision. The request takes no amount. If releasing the seats fails nothing changes (503, retry with the same key). A booking that is already cancelled answers 200 with `already_cancelled` and does not count against the daily limit. Cannot be undone.

Scope: `bookings.cancel:write`.
Needs an `Idempotency-Key` header. Counts against the key's daily cancels limit.

Path parameters:

- `id` (string): Booking id (UUID) from GET /api/v1/bookings.

Request body (JSON):

- `reason` (string): Why the booking is cancelled. Kept on the booking for your team.

Response fields (200):

- `id` (string): The booking id.
- `status` (one of `CONFIRMED`, `CANCELLED`, `REFUNDED`): The booking's status now. `REFUNDED` once the money is back with the guest, `CANCELLED` when nothing needed returning or a void is still settling, `CONFIRMED` only with `refund.outcome` `pending`.
- `already_cancelled` (boolean): True when the booking was already cancelled before this call. Nothing changed.
- `refund` (object)
  - `outcome` (one of `refunded`, `none`, `pending`): `refunded`: the payment provider confirmed the money is returned. `none`: nothing was returned by this call (nothing was paid, or it was already cancelled). `pending`: the seats were released but the refund did not complete; Panion has been alerted and completes it by hand.
  - `amount_minor` (integer): Refunded amount in minor units.
  - `currency` (string)

Example: Cancel with a full refund

```sh
curl -X POST "https://www.panion.travel/api/v1/bookings/8f6d2c1e-4b7a-4e3f-9c2d-1a5b6c7d8e9f/cancel" \
  -H "Authorization: Bearer $PANION_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Storm warning, the boat cannot sail"}'
```

```json
{
  "id": "8f6d2c1e-4b7a-4e3f-9c2d-1a5b6c7d8e9f",
  "status": "REFUNDED",
  "already_cancelled": false,
  "refund": {
    "outcome": "refunded",
    "amount_minor": 179900,
    "currency": "NOK"
  }
}
```

Responses:

- 200: Done
- 400: Invalid body, or the Idempotency-Key header is missing
- 401: Missing, invalid, expired or revoked key
- 403: The key lacks the scope
- 404: Not found, or not yours
- 409: A request with this Idempotency-Key is still running
- 422: Not possible for this booking, or the Idempotency-Key was used for a different request
- 429: Rate limited, or the key's daily limit for this action is reached (cancels). Retry-After says when to try again.
- 503: A system this action depends on did not answer; retry with the same key

### POST /api/v1/bookings/{id}/reschedule

Move a booking to another date or departure.

Moves a live Panion booking in your booking system and on Panion, at the same price: the payment is never touched, so a departure with a different price is refused (422). The guest's free-change window does not apply to you, but a tour that has started cannot be moved, the new date must be available, and a pay-later booking still owing its balance cannot move too close to the charge deadline. Pick a departure with GET /api/v1/bookings/{id}/reschedule-options. The guest is emailed that you moved the booking and your team gets the usual reschedule email. A booking already on that date answers 200 with `already_on_date` and does not count against the daily limit.

Scope: `bookings.reschedule:write`.
Needs an `Idempotency-Key` header. Counts against the key's daily reschedules limit.

Path parameters:

- `id` (string): Booking id (UUID) from GET /api/v1/bookings.

Request body (JSON):

- `date` (string, YYYY-MM-DD): The new departure day (YYYY-MM-DD).
- `start_time_id` (string, optional): `start_time_id` of an option from GET /api/v1/bookings/{id}/reschedule-options. Omit to keep the booking's time of day.
- `rate_id` (string, optional): `rate_id` of the same option, when it has one.

Response fields (200):

- `id` (string): The booking id.
- `status` (string)
- `date` (string): The booking's departure day now.
- `start_time` (string, nullable): Its start time now, when known.
- `previous_date` (string, nullable)
- `previous_start_time` (string, nullable)
- `already_on_date` (boolean): True when the booking was already on that date. Nothing changed.

Example: Move to another evening

```sh
curl -X POST "https://www.panion.travel/api/v1/bookings/8f6d2c1e-4b7a-4e3f-9c2d-1a5b6c7d8e9f/reschedule" \
  -H "Authorization: Bearer $PANION_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"date":"2026-12-14","start_time_id":"4821","rate_id":"1093"}'
```

```json
{
  "id": "8f6d2c1e-4b7a-4e3f-9c2d-1a5b6c7d8e9f",
  "status": "CONFIRMED",
  "date": "2026-12-14",
  "start_time": "18:00",
  "previous_date": "2026-12-12",
  "previous_start_time": "18:00",
  "already_on_date": false
}
```

Responses:

- 200: Done
- 400: Invalid body, or the Idempotency-Key header is missing
- 401: Missing, invalid, expired or revoked key
- 403: The key lacks the scope
- 404: Not found, or not yours
- 409: A request with this Idempotency-Key is still running
- 422: Not possible for this booking, or the Idempotency-Key was used for a different request
- 429: Rate limited, or the key's daily limit for this action is reached (reschedules). Retry-After says when to try again.
- 503: A system this action depends on did not answer; retry with the same key
