# Stowbo · Money

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

**Hold the money on a booking**

`operationId: StowboController_holdPayment`

Puts a booking's money on hold. Two separate things stop: the guest's card stays authorised but is not captured, and — the part that matters — **the host is not paid at settle**.

Once money reaches a host wallet and is withdrawn, a damage claim has nothing to claw back from, so a contested stay must be held *before* it settles.

A hold does not forgive anything. The guest can still be charged and what is owed is still owed.

#### Signature

```http
POST /stowbo/booking/{bookingId}/hold (bookingId: string, body) -> The hold that was recorded on the booking
```

#### Access

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

#### Notes

- Holding an already-held booking overwrites the previous hold, including who placed it and when.

#### 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}/release-hold`
- `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. |

### Request body

Why the money is being held.

```json
{
  "reason": "damage_claim",
  "note": "Guest reports water damage to stored items"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The hold that was recorded on the booking |
| `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
{
  "hold": {
    "reason": "damage_claim",
    "note": "Guest reports water damage to stored items",
    "heldAt": "2026-09-04T10:00:00.000Z",
    "heldBy": "ops@venue.com"
  }
}
```

## POST /stowbo/booking/{bookingId}/release-hold

**Release a payment hold**

`operationId: StowboController_releaseHold`

Clears the hold so the booking settles and pays out normally. The release is recorded on the timeline with any note you pass.

#### Signature

```http
POST /stowbo/booking/{bookingId}/release-hold (bookingId: string, body) -> Confirmation the hold was cleared
```

#### Access

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

#### Notes

- Releasing a booking that has no hold succeeds and returns `{ released: true }` — it is not an error.

#### 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}/hold`

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

Optional note explaining the release.

```json
{
  "note": "Claim withdrawn"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Confirmation the hold was cleared |
| `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
{
  "released": true
}
```

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

**Charge the guest now**

`operationId: StowboController_chargeGuest`

Adds a charge and attempts to take it immediately, without waiting for settle.

**The ledger is written first, the capture second.** If the capture fails, the charge still stands as owed — that is the honest state, and silently dropping a failed capture is how a platform ends up absorbing damage costs it had already recorded. The response tells you which happened via `captured` and `captureError`, and a `200` does **not** mean the money was taken.

The guest is notified either way, with wording that reflects whether it was charged or added to their balance.

#### Signature

```http
POST /stowbo/booking/{bookingId}/charge (bookingId: string, body) -> The ledger entry, the new total, and whether the capture succeeded
```

#### Access

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

#### Notes

- Always check `captured`. A successful response with `captured: false` means the guest owes the money but has not paid it.
- Not idempotent — each call adds another ledger line.

#### 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 | amount must be positive | `amount` is missing, zero or negative. | Send a positive amount. Use a refund to move money the other way. |

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

#### See also

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

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

What to charge, and why.

```json
{
  "amount": 25,
  "category": "damage",
  "description": "Replacement lock",
  "line": 0
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The ledger entry, the new total, and whether the capture succeeded |
| `400` | amount must be positive — `amount` is missing, zero or negative. |
| `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
{
  "entry": {
    "kind": "adjustment",
    "code": "damage",
    "amount": 25,
    "bearer": "guest"
  },
  "total": 70,
  "captured": false,
  "captureError": "card_declined"
}
```

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

**Cancel a booking**

`operationId: StowboController_cancelBooking`

Cancels a booking and unwinds it in a fixed order: every held unit is released back to `vacant`, the guest's uncaptured authorisation is voided, the refund is issued, and the deposit — which is a hold, not a charge — comes back in full, because nothing was stored to damage.

The refund amount follows the cancellation policy unless you override it. Inside the free-cancellation window the guest gets everything back; outside it, the fraction the policy promised. Pass `refundAmount` to override both.

Any cancellation or no-show fee the policy charges is added to the bill, and the bill is released down to what is kept plus what is still due.

The record is kept. Cancellations are evidence, so the booking is marked `cancelled` (or `no_show`) rather than deleted, and the refund goes back through the gateway rather than by rewriting the total.

#### Signature

```http
POST /stowbo/booking/{bookingId}/cancel (bookingId: string, body) -> The cancelled booking record
```

#### Access

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

#### Notes

- `refundAmount` overrides the policy completely, including the free-cancellation window. Send `0` to cancel with no refund.
- The refund runs against the charges actually recorded for the booking, so a guest who never paid receives nothing regardless of the policy.

#### 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` | ALREADY_CLOSED | Cannot cancel a <status> booking | The booking is already `settled` or `cancelled`. | A settled booking is refunded rather than cancelled — use `POST /stowbo/booking/{bookingId}/refund` or `POST /stowbo/transactions/{ref}/refund`. |

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

#### See also

- `POST /stowbo/booking/{bookingId}/refund`
- `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

Cancellation details. All optional.

```json
{
  "reason": "Guest no longer needs the space"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The cancelled booking record |
| `400` | Cannot cancel a <status> booking — The booking is already `settled` or `cancelled`. |
| `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}/settle

**Settle a booking**

`operationId: StowboController_settle`

The final charge: adds any overstay, splits host-earnable money at the take rate and credits the host wallet. A held booking (`hold`) settles but does not pay the host. Refused while a balance is still owed. The customer is emailed the closing statement.

#### Signature

```http
POST /stowbo/booking/{bookingId}/settle (bookingId: string) -> The settlement
```

#### 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. |
| `409` | BOOKING_UNPAID | Can't settle — <balance> <currency> still owed. Collect payment first. | The booking has an outstanding balance. The body carries `total`, `paid` and `balance`. | Take payment or send a payment request first. |

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

#### See also

- `POST /stowbo/booking/{bookingId}/hold`
- `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. |
| `bookingId` | path | string | yes | Booking `sk` or reference name. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The settlement |
| `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. |
| `409` | Can't settle — <balance> <currency> still owed. Collect payment first. — The booking has an outstanding balance. The body carries `total`, `paid` and `balance`. |
| `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}/adjustments

**Get the bill, line by line**

`operationId: StowboController_listAdjustments`

Every adjustment on the booking — charges, credits, discounts, refunds and their reversals — and their sum.

#### Signature

```http
GET /stowbo/booking/{bookingId}/adjustments (bookingId: string) -> { adjustments, total }
```

#### 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}/adjustments`

### 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` | { adjustments, total } |
| `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}/adjustments

**Put a line on the bill**

`operationId: StowboController_addAdjustment`

Adds a line to the booking's bill. `bearer` decides who absorbs it: comping a customer costs the platform, a damage charge pays the host. Attach evidence in `files` — a damage charge with no photo does not survive a chargeback.

#### Signature

```http
POST /stowbo/booking/{bookingId}/adjustments (bookingId: string, body) -> The entry and the booking's new total and balance (plus `refunded`/`refundOk` when a credit refunded money)
```

#### 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` | AMOUNT_REQUIRED | amount is required | `amount` is missing or zero. | — |

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

#### See also

- `PUT /stowbo/booking/{bookingId}/adjustments/{index}/reverse`

### 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
{
  "amount": 25,
  "code": "damage",
  "label": "Broken lock",
  "bearer": "guest"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The entry and the booking's new total and balance (plus `refunded`/`refundOk` when a credit refunded money) |
| `400` | amount is required — `amount` is missing or zero. |
| `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. |

## PUT /stowbo/booking/{bookingId}/adjustments/{index}/reverse

**Undo a line on the bill**

`operationId: StowboController_reverseAdjustment`

Adds the opposite of a line rather than deleting it — the original and the correction both stay visible.

#### Signature

```http
PUT /stowbo/booking/{bookingId}/adjustments/{index}/reverse (bookingId: string, index: string, body) -> { reversal, total }
```

#### 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. |
| `409` | ALREADY_REVERSED | Already reversed | That line has already been reversed. | — |

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. |
| `index` | path | string | yes | Zero-based index of the line in the bill. |

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { reversal, total } |
| `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. |
| `409` | Already reversed — That line has already been reversed. |
| `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}/refund

**Refund a booking**

`operationId: StowboController_refundBooking`

With `cancel: true` this is a cancellation (`POST /stowbo/booking/{bookingId}/cancel`): `full` refunds everything paid, `amount` refunds that much, neither follows the policy. Without `cancel`, the money goes back as a credit line on the bill and the stay continues; `amount` is required and cannot exceed what has been paid.

#### Signature

```http
POST /stowbo/booking/{bookingId}/refund (bookingId: string, body) -> The cancelled booking (with `cancel`), or the credit entry and new 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. |
| `409` | NOTHING_PAID | Nothing has been paid on this booking — add a credit adjustment instead of a refund | No payment recorded. | — |

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

#### See also

- `POST /stowbo/booking/{bookingId}/cancel`
- `POST /stowbo/transactions/{ref}/refund`

### 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
{
  "amount": 10,
  "reason": "Late handover"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The cancelled booking (with `cancel`), or the credit entry and new totals |
| `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. |
| `409` | Nothing has been paid on this booking — add a credit adjustment instead of a refund — No payment recorded. |
| `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}/payment-request

**Send a payment link for a booking**

`operationId: StowboController_paymentRequest`

Raises a payment request as the booking's host and sends the link to the customer. Defaults to the booking's balance and the booking's customer; `send: false` creates the link without sending it.

#### Signature

```http
POST /stowbo/booking/{bookingId}/payment-request (bookingId: string, body) -> The 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. |
| `400` | AMOUNT_REQUIRED | amount is required | Nothing is owed and no `amount` was sent. | — |

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

#### See also

- `POST /stowbo/transactions/{requestId}/cancel`

### 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
{
  "send": true
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The request |
| `400` | amount is required — Nothing is owed and no `amount` was sent. |
| `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}/take-payment

**Record a payment taken on a booking**

`operationId: StowboController_takePayment`

Records a card payment taken for the booking, as its host, from a confirmed `paymentIntentId` (card reader or Tap to Pay). The intent is verified with the gateway; recording the same intent twice returns `alreadyRecorded`. The operator never types card details.

#### Signature

```http
POST /stowbo/booking/{bookingId}/take-payment (bookingId: string, body) -> { ok, paymentRef, amount, currency, category, booking } or { ok, alreadyRecorded, paymentRef }
```

#### 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` | PAYMENT_INTENT_REQUIRED | paymentIntentId is required | No intent sent. | — |

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
{
  "paymentIntentId": "pi_3Abc123",
  "category": "balance"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { ok, paymentRef, amount, currency, category, booking } or { ok, alreadyRecorded, paymentRef } |
| `400` | paymentIntentId is required — No intent sent. |
| `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. |

