# Supply API (v1)

The Supply API lets an approved property management or reservation system maintain OutReserve property content, inventory units, nightly rates, and calendar inventory. It is designed as the first outlet for OutReserve's planned channel manager. The base path is `/api/v1/supply`.

**Preview boundary:** `1.0.0-preview.1` is a guarded content/ARI contract, not proof of an available sandbox or partner certification. Property, unit, and availability/rate/inventory (ARI) writes are the Phase 1 scope. A generic Supply connection is not bookable in Phase 1. A bounded reservation pull/ack implementation exists in this worktree but is not in the released OpenAPI/Postman contract and is not booking-ready; see [reservations.md](reservations.md). The separately labelled `reservation-preview-openapi.yaml`, `postman/reservation-preview.postman_collection.json`, and `reservation-preview-error-matrix.json` remain checkout-local or operator-only downloads for authorized first-client testing. They are not advertised in the public agent catalog, and operator responses are private and uncacheable. These artifacts do not enable the routes or authorize live booking traffic. Approval to import supply does not imply permission to accept OutReserve bookings.

## Start here

1. Obtain a Supply credential and use `Authorization: Bearer <key>` over HTTPS. Query-string API keys and mobile tokens are not supported for these endpoints. Keep keys server-side.
2. Read `GET /api/v1/supply/connection` before mapping fields. A credential with `supply:read`, `supply:reservations:read`, or `supply:reservations:ack` can inspect its own connection even during a sales suspension; revoked connections and suspended partners remain closed. Its `capabilities` report supported unit types, field rules, calendar limits, and features. `granted_scopes` reports the scopes of the Bearer credential used for this request, not every key on the connection.
   In the private preview, `capabilities.reservations` reflects the gated feed's availability for this account; `capabilities.booking_ready` reflects whether at least one property has an operator-activated reservation outlet. Property and unit `booking_ready` fields are narrower. None of these flags certifies a partner integration or guarantees a particular date is sellable.
3. Discover the exact `country_code`, `state_code`, and `destination_slug` through `GET /api/v1/supply/locations` before creating a property. This read-only catalog includes destinations without consumer listings; see [locations.md](locations.md) for hierarchy, pagination, and error rules. Then create a property using your account-scoped external identifier, add units, and submit it for OutReserve approval. An existing OutReserve property requires a reviewed association; knowing its ID does not grant control.
   Supply property creation requires an explicit, valid IANA `timezone`; it is immutable because ARI dates are property-local.
   For property amenities, discover canonical IDs through `GET /api/v1/supply/amenities` and submit the complete desired `amenity_ids` set; see [amenities.md](amenities.md). Only `writable: true` catalog entries are accepted by the minimal v1 field.
4. Once OutReserve approves the property for supply updates, send absolute ARI values for a bounded date range. Read the returned `calendar_revision` and use it in the next write. Reconcile with `GET /properties/{property_id}/ari?date_from=YYYY-MM-DD&date_to=YYYY-MM-DD`.
5. Use [errors.md](errors.md) and the [released endpoint error matrix](endpoint-error-matrix.md) to decide whether to correct, reconcile, wait, or retry. Revision and reservation-cursor conflicts return machine-readable `context` values; never parse localized prose for a sequence. Every property/unit/media create, existing-property association, review submission, and ARI write requires an `Idempotency-Key`. Persist the exact URL, raw JSON body, and key for each logical operation until its outcome is known; an identical retry returns the original result.

The [OpenAPI contract](openapi-v1.yaml), [Postman Collection](postman/supply-v1.postman_collection.json), and [examples](examples/README.md) are public preview assets. The Agent Skill source at `docs/api/supply/skills/outreserve-supply/SKILL.md` is checkout-local. Use the versioned contract and examples together once OutReserve has configured an authorized test environment and credential; the general read-only Developer API has different authentication options. The local, offline toolkit can be installed from this checkout with `npx --offline --yes --package ./docs/api/supply outreserve-supply-toolkit install --dest .agents/skills/outreserve-supply`. No public npm package has been published.

The checkout-local first-client certification harness at `docs/api/supply/certification/README.md` provides a resumable, version-pinned public-HTTP test path for an isolated, authorized outlet. It does not replace operator activation, a durable PMS receiver, Stripe/guest certification, or live release evidence.

## Core model

| Public concept | Meaning |
| --- | --- |
| Property | The approved listing and its configuration, operating period, currency, and time zone. |
| Unit | An inventory product staged for future sale. `inventory_mode=physical` represents one identifiable site; `inventory_mode=pooled` represents interchangeable stock. The OutReserve `unit_type` is an accommodation category, distinct from a PMS room-type ID. |
| Available count | The absolute count the partner reports for a calendar date. Physical units accept `0` or `1`; pooled units accept `0..capacity`. Public `capacity` maps to the existing internal unit quantity. |
| Incorporated reservation sequence | The latest durably applied OutReserve reservation event for this property reflected in each unit/date absolute count. Every count write includes this value; it cannot move backward or exceed the property's applied cursor. `0` means no OutReserve reservation event is incorporated. Sequence numbers are assigned across the connection, so unrelated properties can create gaps. The wire number is capped at 9,007,199,254,740,991 so JavaScript can retain it exactly. |
| Stop sell | An independent closure at property or unit level. Reopening it does not increase reported inventory or clear other restrictions. |
| Rate | A nightly amount in the property's configured currency, expressed in integer minor units. A zero-decimal currency still uses its own minor unit. |
| Calendar revision | An optimistic-concurrency token for the property's ARI state. |

Unknown inventory is closed. No row for a date, omitted dates in a resynchronization, and a missing count do not create availability. The same physical stock must not be published both as individual units and as a pool.

## Channel-manager compatibility boundary

This first release is intentionally a *minimal* mapping, not full hotel ARI parity. One OutReserve Unit is one inventory product with one default nightly base rate and optional date-specific nightly override. It does not yet model a separate PMS rate-plan ID, occupancy-based prices, derived/nonrefundable plans, length-of-stay price ladders, tax/fee breakdowns, or package inclusions. A channel manager must select and document one canonical source plan per Unit; it must not collapse several materially different plans into one OutReserve rate or double-publish one stock pool. The connection capabilities expose supported unit and ARI fields, but not a rate-plan feature; an absent capability is unsupported.

For a Supply-owned property, nightly price precedence is unit/date `rate_minor`, property/date `rate_minor`, unit `default_rate_minor`, then property `default_rate_minor`. A pre-existing OutReserve Unit `rate` is only a legacy display projection, never an implicit partner rate. If all four Supply sources are absent, readback reports `rate_unknown`, quotes have no price, and an operator cannot activate the reservation outlet (`reservation_rate_incomplete`). An adopted property's PMS must explicitly send its source rate before cutover.

Certification should check each partner mapping for physical versus pooled stock identity, unit type and RV-only fields, currency and property time zone, default versus dated rate precedence, unknown inventory closure, restrictions, duplicate/stale write recovery, and reservation-feed readiness before activation. If the partner requires multiple independent plans, per-occupancy pricing, or a tax/fee contract, keep that product unmapped until a versioned extension is designed. [Booking.com's pricing-type documentation](https://developers.booking.com/connectivity/docs/understanding-pricing-types) illustrates why room type and rate plan are distinct concepts; its [reservation retrieval/acknowledgement model](https://developers.booking.com/connectivity/docs/reservations-api/reservations-overview) informs the proposed Phase 2 boundary, not a Phase 1 promise.

## ARI write semantics

`POST /properties/{property_id}/ari` applies a bounded set of absolute updates atomically. An update can address a unit, or a property-level rate or stop sell. A property-level inventory count is invalid because it cannot be safely apportioned across units. `date_from` and `date_to` are inclusive property-local calendar dates; departure dates in booking stays remain exclusive. Within one update, omitted fields retain their existing value. There is no destructive-by-omission snapshot mode.

Send a fresh 8–128 character `Idempotency-Key` for each logical write and the quoted current calendar revision in `If-Match`. An exact retry returns the original outcome. A reused key with a different payload fails. A stale revision requires readback and reconciliation. Success means the full batch has been applied; validation failure changes nothing. Large initial calendars should be split into bounded date windows and sent sequentially per property. Different properties may synchronize in parallel. An optional request-level `currency` must equal the property's currency.

Read `calendar_revision` from the JSON response and quote that number in the next **new** logical write's `If-Match`. An exact retry retains the original raw body, idempotency key, and quoted `If-Match`, even if a later readback or successful response has advanced the shared revision. The Postman collection saves separate `physicalIfMatch` and `poolIfMatch` values when it first creates each ARI key and never clears them automatically; manually clear only the matching key after its outcome is confirmed or reconciled. Do not use a response `ETag` as the calendar revision: reservation events, temporary holds, activation, and inventory freshness can change effective sellability without changing that revision.

`available_count` is an integer and can never be `null`. `rate_minor` and property/unit `default_rate_minor` are integers from 1 through 100,000,000; zero-priced nights are unsupported in v1. `stop_sell`, `closed_to_arrival`, and `closed_to_departure` are booleans. `min_stay` and `max_stay` are integers from 1 through 365. Explicit `null` clears only an existing rate, minimum-stay, or maximum-stay override; omission means no change. Entries cannot overlap on the same target/date, even if they change different fields. The initial expanded limit is 1,000 scope-days per request and is advertised through capabilities; split larger changes into smaller requests. Phase 1 writes target today through 730 days ahead in the property's time zone.

Readback reports accepted source values and a bounded daily net count. An unactivated generic Supply unit has effective sellable count zero. For an explicitly activated outlet, readback subtracts unincorporated OutReserve reservation changes and active unpaid/amendment holds, then closes missing or stale counts, feed gaps, local blocks, and inactive inventory. `booking_enabled` means the property outlet was activated; it does not promise that every day is open. `blocking_reasons` are calendar-state codes, not HTTP problem codes. Arrival, departure, minimum/maximum-stay, and whole-stay availability still require a date-range quote and final locked checkout; a positive daily count is not a reservation guarantee.

For physical and pooled units, effective sellability is also capped by the places left after open OutReserve stays and live checkout/amendment holds. A partner count greater than that physical remainder cannot make another booking possible. Reducing a pool's `capacity` below overlapping stays returns `409 reservation_pool_capacity_conflict`; if a booking row is concurrently locked, `503 reservation_capacity_review_busy` is retryable and leaves the unit unchanged.

Every unit inventory count must pair with `incorporated_reservation_sequence`, a nonnegative JSON integer. It asserts that the absolute count for each expanded unit/date includes all OutReserve reservation allocation changes for that property through the stated connection-wide sequence; it cannot exceed that property's applied cursor or regress for that unit/date. Unrelated properties may occupy intervening sequence numbers. A watermark cannot be sent without a count or at property scope. A count snapshot updates `inventory_received_at` even when its numerical value is unchanged; rate/restriction-only writes do not refresh inventory. Legacy rows with a count but no watermark or receipt timestamp remain unverified and non-bookable.

## Property and unit lifecycle

New properties start in a pending approval state. Partners may correct content and add units/media before submission; ARI writes require OutReserve approval. Partner deactivation is a status change. Records, mappings, audit events, and historical rates remain retained; the API offers no `DELETE`. Unit creation and update must follow the typed field rules returned by capabilities. Unsupported fields are rejected with field-level errors, not silently ignored.

Adding a unit after a property's reservation outlet is active still creates a staged, closed unit. Backfill its complete fresh ARI, then request an OutReserve operator review. The operator publishes only that unit after checking certification references and explicitly approving its stop-sell removal. The partner API cannot activate a unit by PATCH; see [reservation activation](reservations.md#state-machine-and-activation-gate).

Editing a unit's descriptive metadata does not close it or clear its existing stop-sell state. Explicit `status: inactive` closes the unit; reopening an inactive unit requires an OutReserve-reviewed recovery path.

## Operational rules

- `external_property_id` is unique within the authenticated connection. `external_unit_id` is unique **within its property**, not connection-wide. A PMS serving several customers must keep each account's property mapping isolated and pair every unit ID with its property UUID.
- Use one authenticated connection per approved partner/account scope. OutReserve may revoke or rotate credentials without changing property identifiers.
- For ARI, prefer absolute updates to deltas. Treat timeout outcomes as unknown, retry the identical request with the identical idempotency key, then read back the revision.
- Onboarding create/association/submission writes also require an 8–128 character key. Exact method, path, and raw body retries return the original HTTP status and JSON body, including the original media and operation UUIDs, after current credential and property-access checks. A revoked key, suspended connection, or revoked property mapping can instead return an authorization or visibility error. A different method, path, or body with the same key returns `409 idempotency_key_reused`; use a new key for a changed logical operation. The response header `Idempotency-Replayed: true` identifies a replay. The body is the original result, so a media import may have progressed since the returned `processing_status`; poll the media/operation resource for current state.
- Apply exponential backoff with jitter to `429` and retryable `5xx` responses. Respect `Retry-After` when supplied. Treat connection failures and non-JSON upstream errors as unknown outcomes.
- Do not log API keys, guest data, or whole unredacted HTTP bodies. Include the returned `request_id` in support reports.

See [integration-testing.md](integration-testing.md) for the ordered Postman/API acceptance runbook, [workflows.md](workflows.md) for integration flows, and [errors.md](errors.md) for the error contract.
