# Operations (writes)

> In active development. This is the specification.

Every write is one endpoint.

```
POST /api/public/v1/operations/{operation}
Authorization: Bearer hvat_...
Idempotency-Key: <uuid>
Content-Type: application/json
User-Agent: YourProduct/1.2.0 (you@example.com)
```

The body is the operation's input. The response is the standard envelope.

## Why one endpoint rather than REST verbs

Thirty write routes are thirty places to forget the scope check, the source-of-truth lock, the read-only backstop or the idempotency ledger. One route is one place, and the four checks run in a fixed order before any operation body executes.

It also means an operation name is a stable, machine-readable identifier for a capability, which is what lets the same catalog drive the REST surface and, later, an MCP server without a second implementation.

## The catalog

Operation names are permanent once published. Each maps to the same internal action the Haven dashboard calls, so a write here fans out identically: channel manager push, guest notification, pricing sync.

### Calendar

| Operation       | Scope            | Domain         |
| --------------- | ---------------- | -------------- |
| `block_dates`   | `calendar:write` | `AVAILABILITY` |
| `unblock_dates` | `calendar:write` | `AVAILABILITY` |

### Rates

| Operation                         | Scope         | Domain  |
| --------------------------------- | ------------- | ------- |
| `create_price_override`           | `rates:write` | `RATES` |
| `update_price_override`           | `rates:write` | `RATES` |
| `delete_price_override`           | `rates:write` | `RATES` |
| `clear_price_overrides_for_range` | `rates:write` | `RATES` |
| `create_promotion`                | `rates:write` | `RATES` |
| `update_promotion`                | `rates:write` | `RATES` |
| `delete_promotion`                | `rates:write` | `RATES` |
| `delete_holiday_price_bump_rule`  | `rates:write` | `RATES` |

### Stay rules

| Operation                     | Scope              | Domain          |
| ----------------------------- | ------------------ | --------------- |
| `create_min_stay_override`    | `stay-rules:write` | `BOOKING_RULES` |
| `update_min_stay_override`    | `stay-rules:write` | `BOOKING_RULES` |
| `delete_min_stay_override`    | `stay-rules:write` | `BOOKING_RULES` |
| `create_weekly_min_stay_rule` | `stay-rules:write` | `BOOKING_RULES` |
| `update_weekly_min_stay_rule` | `stay-rules:write` | `BOOKING_RULES` |
| `delete_weekly_min_stay_rule` | `stay-rules:write` | `BOOKING_RULES` |
| `sync_weekly_min_stay_rules`  | `stay-rules:write` | `BOOKING_RULES` |
| `set_check_in_out_blocks`     | `stay-rules:write` | `BOOKING_RULES` |

### Listings

| Operation                  | Scope              | Domain                                |
| -------------------------- | ------------------ | ------------------------------------- |
| `update_listing_content`   | `listings:write`   | `PROPERTY_CONTENT`, `LISTING_CONTENT` |
| `set_listing_market_state` | `listings:publish` | `PROPERTY_CONTENT`                    |

### Reservations

| Operation              | Scope                 | Confirmation |
| ---------------------- | --------------------- | ------------ |
| `approve_booking`      | `reservations:write`  |              |
| `respond_bundle_quote` | `reservations:write`  |              |
| `cancel_reservation`   | `reservations:cancel` |              |
| `issue_booking_refund` | `reservations:refund` | required     |
| `adjust_booking_price` | `reservations:refund` | required     |

### Discounts

| Operation              | Scope             |
| ---------------------- | ----------------- |
| `update_discount_code` | `discounts:write` |
| `toggle_discount_code` | `discounts:write` |
| `delete_discount_code` | `discounts:write` |

### Messaging

| Operation                | Scope             |
| ------------------------ | ----------------- |
| `send_host_message`      | `messaging:send`  |
| `mark_conversation_read` | `messaging:write` |

### Blog

| Operation             | Scope        |
| --------------------- | ------------ |
| `upsert_blog_post`    | `blog:write` |
| `unpublish_blog_post` | `blog:write` |

Neither declares a source-of-truth domain: a blog is Haven's own surface and no channel manager can own it, so the `domain_locked` check never applies. The full contract — what the body may contain, what happens to styling, and why a publish can come back as a draft — is on [Blog publishing](05-blog-publishing.md).

## Order of refusal

Checks run cheapest first, and a request that fails an earlier one never reaches a later one. This matters because it means a rejected write never touches the database and never consumes an idempotency record.

1. Is the operation a real one? If not, `404`.
2. Does your token carry the scope it declares? If not, `403` with `insufficient_scope`.
3. May the host's role write at all? If not, `403`.
4. Does an external system own the domain for this listing? If so, `409` with `domain_locked`.
5. Claim the idempotency key.
6. Run the operation.

## Confirmation

Two operations move money and require an extra step: `issue_booking_refund` and `adjust_booking_price`. Call the operation with `"confirm": false` to receive a description of exactly what would happen and a single-use confirmation token, then call again with that token to commit.

A partner-initiated refund is the one class of write where a bug is unrecoverable, so it does not happen in one call.

## Codes in, ids out

Operations take reference codes for top-level objects: `propertyCode`, `reservationCode`, `conversationCode`. Child rows are addressed by the integer id you read from the parent. Sending a raw internal id where a code is expected is refused rather than accepted.

## Not available to any application

No operation reaches billing, subscriptions, payout destinations, the advertising wallet, team membership, workspace structure, or account deletion. These are not missing from the catalog; they are refused by scope, and the refusals are documented in the [Scope reference](../02-scopes/02-scope-reference.md).
