# Stowbo · Stay

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /stowbo/booking/{bookingId}/extend

**Extend a stay**

`operationId: StowboController_extendStay`

Extends a space line to a later end date and re-prices it.

The order of operations is deliberate and matters:

1. The new window is **priced but not committed**.
2. The space must be **free for the extra time only** — the interval from the current end to the new end. The booking's own line still ends at the old date, so it is not counted against the window it is growing into. A booking that starts right after this one blocks the extension.
3. The price difference is **collected first**. A declined payment changes nothing — the stay keeps its original window.

A re-price that comes out cheaper or equal (hitting a daily cap, for instance) skips the payment step.

#### Signature

```http
POST /stowbo/booking/{bookingId}/extend (bookingId: string, body) -> The extended line, what was added, and the booking's new total and balance
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The 409 body is structured, not the standard error envelope: it carries `code`, `message`, `available` and, when a closure caused it, `until: "blackout"`.
- Nothing is written until both the availability check and the payment succeed, so a failed extension leaves the booking untouched.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | BOOKING_NOT_FOUND | Booking <id> not found | No booking in the org matches that id or reference. | Check the identifier. `GET /stowbo/custody` lists what is currently held, with its booking id. |
| `400` | END_DATE_REQUIRED | endDate is required | `endDate` is missing. | Send the new end of the stay. |
| `409` | UNAVAILABLE | Cannot extend — the space is not free for that time (N of M available). | The space is booked out, blacked out, or blocked by a parent listing for part of the extra window. | Check `GET /stowbo/listing/{listing}/calendar` for the next free window, or move the guest with `move-unit`. |
| `402` | PAYMENT_REQUIRED | Extending adds <amount> <currency> — payment required. | The extra time costs more and no payment method or intent was sent. | Retry with `paymentMethodId` or a confirmed `paymentIntentId`. The body carries `dueNow` and `currency`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /stowbo/listing/{listing}/calendar`
- `POST /stowbo/booking/{bookingId}/move-unit`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `bookingId` | path | string | yes | Booking `sk` or reference name. |

### Request body

The new end date, and which line to extend.

```json
{
  "endDate": "2026-09-06T17:00:00.000Z",
  "paymentMethodId": "pm_1Abc123"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The extended line, what was added, and the booking's new total and balance |
| `400` | endDate is required — `endDate` is missing. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `402` | Extending adds <amount> <currency> — payment required. — The extra time costs more and no payment method or intent was sent. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Booking <id> not found — No booking in the org matches that id or reference. |
| `409` | Cannot extend — the space is not free for that time (N of M available). — The space is booked out, blacked out, or blocked by a parent listing for part of the extra window. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "line": 0,
  "endDate": "2026-09-06T17:00:00.000Z",
  "added": 30,
  "paid": true,
  "total": 75,
  "balance": 0
}
```

## POST /stowbo/booking/{bookingId}/move-unit

**Move stored things to another unit**

`operationId: StowboController_moveUnit`

Reassigns a booking line to a different unit. The old unit is released back to `vacant` and the new one is marked `occupied` against this booking — leaving the old one held is how a unit silently disappears from availability forever.

The guest is notified that their spot changed.

#### Signature

```http
POST /stowbo/booking/{bookingId}/move-unit (bookingId: string, body) -> The line index and its new unit
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The target unit is not checked for being free. Moving into an occupied unit will overwrite its booking reference.
- Because the release happens first, a failure at the lookup step leaves the booking without a held unit. Retry with a valid unit to restore a consistent state.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | BOOKING_NOT_FOUND | Booking <id> not found | No booking in the org matches that id or reference. | Check the identifier. `GET /stowbo/custody` lists what is currently held, with its booking id. |
| `400` | UNIT_REQUIRED | unit is required | `unit` is missing. | Send the target unit. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /stowbo/custody`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `bookingId` | path | string | yes | Booking `sk` or reference name. |

### Request body

The unit to move to.

```json
{
  "unit": "L-22",
  "reason": "Original locker jammed"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The line index and its new unit |
| `400` | unit is required — `unit` is missing. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Booking <id> not found — No booking in the org matches that id or reference. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "line": 0,
  "unit": "L-22"
}
```

## POST /stowbo/booking/{bookingId}/access

**Record an access**

`operationId: StowboController_logAccess`

Appends an access event to the booking timeline — who opened the unit, and when. This is the record that answers "was anyone in there", so it is written on every open regardless of who did it.

It records only; it does not unlock anything or check whether access was permitted.

#### Signature

```http
POST /stowbo/booking/{bookingId}/access (bookingId: string, body) -> The booking, with the event appended
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Audit writes never fail the operation they describe — if the event cannot be appended, the call still succeeds.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | BOOKING_NOT_FOUND | Booking <id> not found | No booking in the org matches that id or reference. | Check the identifier. `GET /stowbo/custody` lists what is currently held, with its booking id. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /stowbo/booking/{bookingId}/timeline`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `bookingId` | path | string | yes | Booking `sk` or reference name. |

### Request body

Who accessed it. Everything is optional — the caller is used as the actor by default.

```json
{
  "actorRole": "guest",
  "line": 0,
  "detail": "Collected two boxes"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The booking, with the event appended |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Booking <id> not found — No booking in the org matches that id or reference. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /stowbo/booking/{bookingId}/timeline

**Get everything that happened to a booking**

`operationId: StowboController_timeline`

The full history of a booking in chronological order, together with its money adjustments and any claims. This is the single read for an audit view — timeline, ledger and disputes in one response.

#### Signature

```http
GET /stowbo/booking/{bookingId}/timeline (bookingId: string) -> The booking history
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | BOOKING_NOT_FOUND | Booking <id> not found | No booking in the org matches that id or reference. | Check the identifier. `GET /stowbo/custody` lists what is currently held, with its booking id. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /stowbo/booking/{bookingId}/access`
- `POST /stowbo/booking/{bookingId}/charge`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `bookingId` | path | string | yes | Booking `sk` or reference name. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The booking history |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Booking <id> not found — No booking in the org matches that id or reference. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /stowbo/booking/{bookingId}/addon

**Add an add-on to a running booking**

`operationId: StowboController_addAddon`

Prices the add-on, charges it, and puts it on the bill. A free add-on skips the gateway but still lands on the order; a service add-on also raises a fulfilment request for the host.

#### Signature

```http
POST /stowbo/booking/{bookingId}/addon (bookingId: string, body) -> The bill entry and totals
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | BOOKING_NOT_FOUND | Booking <id> not found | No booking in the org matches that id or reference. | Check the identifier. `GET /stowbo/custody` lists what is currently held, with its booking id. |
| `400` | INVALID_AMOUNT | An add-on cannot cost less than nothing | `amount` < 0. | — |
| `402` | PAYMENT_REQUIRED | This add-on is <amount> <currency> — payment required. | Not free and no payment method/intent. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `bookingId` | path | string | yes | Booking `sk` or reference name. |

### Request body

```json
{
  "name": "insurance",
  "label": "Insurance",
  "amount": 5,
  "paymentMethodId": "pm_1Abc123"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The bill entry and totals |
| `400` | An add-on cannot cost less than nothing — `amount` < 0. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `402` | This add-on is <amount> <currency> — payment required. — Not free and no payment method/intent. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Booking <id> not found — No booking in the org matches that id or reference. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /stowbo/booking

**Book on a customer's behalf**

`operationId: StowboController_createBooking`

Creates a booking through the same path the customer app takes, recorded with channel `operator`. Availability is re-checked for every line and the booking is refused if any line is full. Platform and host fees and any discount code are priced in. The amount due now is charged to `paymentMethodId` unless `skipPayment` is set, in which case the booking is recorded as owed.

#### Signature

```http
POST /stowbo/booking (body) -> The booking, its items, the total and what was charged now
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | LINES_REQUIRED | At least one line is required | `lines` is empty. | — |
| `409` | UNAVAILABLE | <listing> is not available for that window — N free, M requested | A line does not fit. | Check `GET /stowbo/availability`. |
| `402` | PAYMENT_REQUIRED | A payment method is required — <amount> <currency> due now. | Something is due now, no `paymentMethodId` and no `skipPayment`. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /stowbo/availability`
- `POST /stowbo/booking/{bookingId}/payment-request`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "customer": "ada@example.com",
  "lines": [
    {
      "listing": "downtown-lockers",
      "startDate": "2026-09-05T09:00:00.000Z",
      "endDate": "2026-09-08T17:00:00.000Z",
      "quantity": 1
    }
  ],
  "skipPayment": true
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The booking, its items, the total and what was charged now |
| `400` | At least one line is required — `lines` is empty. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `402` | A payment method is required — <amount> <currency> due now. — Something is due now, no `paymentMethodId` and no `skipPayment`. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `409` | <listing> is not available for that window — N free, M requested — A line does not fit. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /stowbo/booking/{bookingId}/requests/{requestId}

**Answer a customer request**

`operationId: StowboController_respondToRequest`

Moves a request the host has left sitting: `acknowledged` → `ready` → `done`, or `declined`. The customer is told either way.

#### Signature

```http
POST /stowbo/booking/{bookingId}/requests/{requestId} (bookingId: string, requestId: string, body) -> { ok, request }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | BOOKING_NOT_FOUND | Booking <id> not found | No booking in the org matches that id or reference. | Check the identifier. `GET /stowbo/custody` lists what is currently held, with its booking id. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `bookingId` | path | string | yes | Booking `sk` or reference name. |
| `requestId` | path | string | yes | Request id from the booking. |

### Request body

```json
{
  "status": "ready"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { ok, request } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Booking <id> not found — No booking in the org matches that id or reference. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /stowbo/booking/{bookingId}/request

**Raise a request for a customer**

`operationId: StowboController_raiseRequest`

For a customer who phoned in: raises the same request the app would (`out`, `in`, `checkout`, `addon` or `service`), puts it on the booking timeline and notifies the host.

#### Signature

```http
POST /stowbo/booking/{bookingId}/request (bookingId: string, body) -> { ok, request }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | BOOKING_NOT_FOUND | Booking <id> not found | No booking in the org matches that id or reference. | Check the identifier. `GET /stowbo/custody` lists what is currently held, with its booking id. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /stowbo/booking/{bookingId}/requests/{requestId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `bookingId` | path | string | yes | Booking `sk` or reference name. |

### Request body

```json
{
  "type": "out",
  "note": "Collecting at 5pm"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { ok, request } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Booking <id> not found — No booking in the org matches that id or reference. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

