# Hella Partner API

Version **1.0.0-beta.1** · Status **beta** · Updated **2026-07-13**

The Hella Partner API lets approved partners discover properties, check availability,
request an indicative quote and hand the traveller off to Hella.

Every operation requires a server-side partner credential. Browser CORS policy is not
an authentication mechanism. Dates are calendar dates in the property's IANA timezone;
a stay uses the half-open interval `[checkIn, checkOut)`, so check-out is not an occupied night.
Monetary values are decimal strings in the response currency, never floating-point numbers.

Base URL: `https://www.app.hellastays.com/app/api`

## Authentication and limits

Use a revocable Bearer partner token from a server-side environment. Never expose the token in browser code. Traffic first has an ingress safety limit of 300 requests per 60 seconds for each source IP. After authentication, the default partner quota is 120 requests per 60 seconds for each credential.

Every response exposes `X-Correlation-ID` through CORS. After successful credential validation, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` authoritatively describe the partner-credential quota, not the ingress IP limit. On any observable 429, honour `Retry-After`.

## Contract rules

- Property identifiers are opaque strings.
- Exact addresses and coordinates are never returned.
- Dates are real local Gregorian calendar dates in the property timezone.
- Stay intervals are half-open: `[checkIn, checkOut)`.
- A stay contains at most 60 occupied nights.
- Whole-stay availability requires one eligible inventory type for every night; never combine per-day capacity across types.
- Search results are ordered by opaque `propertyId` before pagination.
- Monetary values are decimal strings in the response currency.
- Quotes are indicative and their price is not preserved in the handoff.
- Use the absolute `handoffUrl` unchanged; do not construct it yourself.

## Problem code registry

Clients branch on `code`. The stable values in this contract are: `VALIDATION_ERROR`, `INVALID_JSON`, `PAYLOAD_TOO_LARGE`, `PARTNER_AUTH_REQUIRED`, `INVALID_PARTNER_CREDENTIAL`, `PARTNER_ACCESS_REVOKED`, `PARTNER_SCOPE_INSUFFICIENT`, `ORIGIN_NOT_ALLOWED`, `PROPERTY_NOT_FOUND`, `STAY_UNAVAILABLE`, `QUOTE_UNAVAILABLE`, `RATE_LIMIT_EXCEEDED`, `PARTNER_API_UNAVAILABLE`, `PARTNER_OPERATION_NOT_FOUND`, `HANDOFF_INVALID`, `INTERNAL_ERROR`.

## Operations

<a id="listPartnerProperties"></a>
### GET /v1/properties

Operation ID: `listPartnerProperties`

Required scope: `properties:read`.

Idempotency: **idempotent**. Success cache policy: `private, max-age=60`.

Maximum stay: **60 nights**.

Pagination order: `propertyId`.

Returns only partner-safe catalogue fields. Exact addresses and coordinates are not exposed.
Without dates this operation is catalogue discovery. `checkIn` and `checkOut` must either
both be omitted or both be supplied. With dates, `items` contains only properties available
for `[checkIn, checkOut)` in each property's timezone and able to host `guests` using one
eligible inventory type for the whole stay. Stays may contain at most 60 nights. Results are
ordered by opaque `propertyId` before pagination. Unknown query parameters are rejected with
`VALIDATION_ERROR`.

| Parameter | In | Required | Schema |
|---|---|---:|---|
| `city` | query | yes | `string` |
| `country` | query | no | `string` |
| `checkIn` | query | no | `LocalDate` |
| `checkOut` | query | no | `LocalDate` |
| `guests` | query | no | `number` |
| `page` | query | no | `number` |
| `pageSize` | query | no | `number` |
| `locale` | query | no | `"it-IT" | "en-GB"` |

Success: **200** `PartnerPropertyList`.

Errors: `400`, `401`, `403`, `429`, `500`, `503`. Error media type is `application/problem+json`.

Machine-readable error codes: `400` `VALIDATION_ERROR`; `401` `PARTNER_AUTH_REQUIRED`, `INVALID_PARTNER_CREDENTIAL`, `PARTNER_ACCESS_REVOKED`; `403` `PARTNER_SCOPE_INSUFFICIENT`, `ORIGIN_NOT_ALLOWED`; `429` `RATE_LIMIT_EXCEEDED`; `500` `INTERNAL_ERROR`; `503` `PARTNER_API_UNAVAILABLE`.

#### Success response fields

| Field | Type | Required | Nullable | Description |
|---|---|---:|---:|---|
| `items` | `array<PartnerPropertySummary>` | yes | no | Properties ordered deterministically by opaque propertyId before pagination. |
| `items[].propertyId` | `PropertyId` | yes | no |  |
| `items[].name` | `string` | yes | no |  |
| `items[].description` | `string` | yes | yes |  |
| `items[].propertyType` | `string` | yes | no |  |
| `items[].location` | `PartnerLocation` | yes | no |  |
| `items[].location.city` | `string` | yes | no |  |
| `items[].location.country` | `string` | yes | no |  |
| `items[].location.area` | `string` | yes | yes | Approximate neighbourhood or area; never an exact address. |
| `items[].capacity` | `PartnerCapacity` | yes | no |  |
| `items[].capacity.guests` | `integer` | yes | no |  |
| `items[].capacity.bathrooms` | `number` | yes | no |  |
| `items[].amenities` | `array<string>` | yes | no |  |
| `items[].rating` | `PartnerRating` | yes | yes |  |
| `items[].rating.score` | `number` | yes | no |  |
| `items[].rating.count` | `integer` | yes | no |  |
| `items[].startingPrice` | `DecimalMoney` | yes | yes |  |
| `items[].currency` | `Currency` | yes | no |  |
| `items[].timezone` | `string` | yes | no | IANA timezone used for calendar-date semantics. |
| `pagination` | `Pagination` | yes | no |  |
| `pagination.page` | `integer` | yes | no |  |
| `pagination.pageSize` | `integer` | yes | no |  |
| `pagination.total` | `integer` | yes | no |  |
| `pagination.hasMore` | `boolean` | yes | no |  |

<a id="getPartnerProperty"></a>
### GET /v1/properties/{propertyId}

Operation ID: `getPartnerProperty`

Required scope: `properties:read`.

Idempotency: **idempotent**. Success cache policy: `private, max-age=60`.

Returns the partner-safe detail for one opaque property identifier. Unknown query
parameters are rejected with `VALIDATION_ERROR`.

| Parameter | In | Required | Schema |
|---|---|---:|---|
| `propertyId` | path | yes | `PropertyId` |
| `locale` | query | no | `"it-IT" | "en-GB"` |

Success: **200** `PartnerPropertyDetail`.

Errors: `400`, `401`, `403`, `404`, `429`, `500`, `503`. Error media type is `application/problem+json`.

Machine-readable error codes: `400` `VALIDATION_ERROR`; `401` `PARTNER_AUTH_REQUIRED`, `INVALID_PARTNER_CREDENTIAL`, `PARTNER_ACCESS_REVOKED`; `403` `PARTNER_SCOPE_INSUFFICIENT`, `ORIGIN_NOT_ALLOWED`; `404` `PROPERTY_NOT_FOUND`; `429` `RATE_LIMIT_EXCEEDED`; `500` `INTERNAL_ERROR`; `503` `PARTNER_API_UNAVAILABLE`.

#### Success response fields

| Field | Type | Required | Nullable | Description |
|---|---|---:|---:|---|
| `propertyId` | `PropertyId` | yes | no |  |
| `name` | `string` | yes | no |  |
| `description` | `string` | yes | yes |  |
| `propertyType` | `string` | yes | no |  |
| `location` | `PartnerLocation` | yes | no |  |
| `location.city` | `string` | yes | no |  |
| `location.country` | `string` | yes | no |  |
| `location.area` | `string` | yes | yes | Approximate neighbourhood or area; never an exact address. |
| `capacity` | `PartnerCapacity` | yes | no |  |
| `capacity.guests` | `integer` | yes | no |  |
| `capacity.bathrooms` | `number` | yes | no |  |
| `amenities` | `array<string>` | yes | no |  |
| `rating` | `PartnerRating` | yes | yes |  |
| `rating.score` | `number` | yes | no |  |
| `rating.count` | `integer` | yes | no |  |
| `startingPrice` | `DecimalMoney` | yes | yes |  |
| `currency` | `Currency` | yes | no |  |
| `timezone` | `string` | yes | no | IANA timezone used for calendar-date semantics. |

<a id="getPartnerPropertyAvailability"></a>
### GET /v1/properties/{propertyId}/availability

Operation ID: `getPartnerPropertyAvailability`

Required scope: `availability:read`.

Idempotency: **idempotent**. Success cache policy: `no-store`.

Maximum stay: **60 nights**.

Checks every occupied local calendar night in `[checkIn, checkOut)` using the property's
IANA timezone; check-out is exclusive. `available: true` guarantees that one eligible
inventory type can host the requested guests for the entire interval, without combining
capacity from different inventory types across nights. Stays may contain at most 60 nights.
An unknown property returns 404, never an empty successful response. Unknown query parameters
are rejected with `VALIDATION_ERROR`.

| Parameter | In | Required | Schema |
|---|---|---:|---|
| `propertyId` | path | yes | `PropertyId` |
| `checkIn` | query | yes | `LocalDate` |
| `checkOut` | query | yes | `LocalDate` |
| `guests` | query | no | `number` |

Success: **200** `PartnerAvailability`.

Errors: `400`, `401`, `403`, `404`, `429`, `500`, `503`. Error media type is `application/problem+json`.

Machine-readable error codes: `400` `VALIDATION_ERROR`; `401` `PARTNER_AUTH_REQUIRED`, `INVALID_PARTNER_CREDENTIAL`, `PARTNER_ACCESS_REVOKED`; `403` `PARTNER_SCOPE_INSUFFICIENT`, `ORIGIN_NOT_ALLOWED`; `404` `PROPERTY_NOT_FOUND`; `429` `RATE_LIMIT_EXCEEDED`; `500` `INTERNAL_ERROR`; `503` `PARTNER_API_UNAVAILABLE`.

#### Success response fields

| Field | Type | Required | Nullable | Description |
|---|---|---:|---:|---|
| `propertyId` | `PropertyId` | yes | no |  |
| `stay` | `Stay` | yes | no |  |
| `stay.checkIn` | `LocalDate` | yes | no |  |
| `stay.checkOut` | `LocalDate` | yes | no |  |
| `stay.nights` | `integer` | yes | no | Number of occupied local calendar nights in the half-open stay interval. |
| `stay.guests` | `integer` | yes | no | Number of travellers used for the availability or quote decision. |
| `stay.timezone` | `string` | yes | no | IANA timezone defining the local-calendar interpretation of checkIn and checkOut. |
| `available` | `boolean` | yes | no | True only when one eligible inventory type can host the requested guests for every night in the interval without switching types. |
| `days` | `array<AvailabilityDay>` | yes | no | One diagnostic entry per occupied local calendar night in [checkIn, checkOut), interpreted in stay.timezone; check-out is excluded. |
| `days[].date` | `LocalDate` | yes | no |  |
| `days[].available` | `boolean` | yes | no | Whether at least one eligible inventory type is available on this local calendar night. Do not combine day flags to infer whole-stay availability. |

<a id="createPartnerQuote"></a>
### POST /v1/quotes

Operation ID: `createPartnerQuote`

Required scope: `quotes:write`.

Idempotency: **non-idempotent**. Success cache policy: `no-store`.

Maximum stay: **60 nights**.

Creates an indicative quote for an available stay. The quoted price is not guaranteed and
is not preserved during handoff; Hella revalidates availability and price before confirmation.
The returned absolute `handoffUrl` contains a signed, opaque partner reference. Partners
must use it unchanged and must not construct handoff links manually. Locale, stay, guests
and attribution are bound inside the opaque `handoff` value rather than exposed as query fields.
Unknown JSON fields are rejected. Malformed JSON returns `INVALID_JSON`; a body above the
accepted request-size limit returns `PAYLOAD_TOO_LARGE`. Quote creation is not idempotent:
a retry can create a new `quoteId`, and this version does not accept an idempotency key.
The requested half-open stay may contain at most 60 local calendar nights.

Request body: `CreatePartnerQuoteRequest` (required).

#### Request body fields

| Field | Type | Required | Nullable | Description |
|---|---|---:|---:|---|
| `propertyId` | `PropertyId` | yes | no |  |
| `stay` | `QuoteStayRequest` | yes | no |  |
| `stay.checkIn` | `LocalDate` | yes | no |  |
| `stay.checkOut` | `LocalDate` | yes | no |  |
| `stay.guests` | `integer` | yes | no |  |
| `locale` | `"it-IT" \| "en-GB"` | yes | no |  |

Success: **201** `PartnerQuote`.

Errors: `400`, `401`, `403`, `404`, `409`, `413`, `429`, `500`, `503`. Error media type is `application/problem+json`.

Machine-readable error codes: `400` `VALIDATION_ERROR`, `INVALID_JSON`; `401` `PARTNER_AUTH_REQUIRED`, `INVALID_PARTNER_CREDENTIAL`, `PARTNER_ACCESS_REVOKED`; `403` `PARTNER_SCOPE_INSUFFICIENT`, `ORIGIN_NOT_ALLOWED`; `404` `PROPERTY_NOT_FOUND`; `409` `STAY_UNAVAILABLE`, `QUOTE_UNAVAILABLE`; `413` `PAYLOAD_TOO_LARGE`; `429` `RATE_LIMIT_EXCEEDED`; `500` `INTERNAL_ERROR`; `503` `PARTNER_API_UNAVAILABLE`.

#### Success response fields

| Field | Type | Required | Nullable | Description |
|---|---|---:|---:|---|
| `quoteId` | `string` | yes | no | Opaque quote identifier. |
| `propertyId` | `PropertyId` | yes | no |  |
| `stay` | `Stay` | yes | no |  |
| `stay.checkIn` | `LocalDate` | yes | no |  |
| `stay.checkOut` | `LocalDate` | yes | no |  |
| `stay.nights` | `integer` | yes | no | Number of occupied local calendar nights in the half-open stay interval. |
| `stay.guests` | `integer` | yes | no | Number of travellers used for the availability or quote decision. |
| `stay.timezone` | `string` | yes | no | IANA timezone defining the local-calendar interpretation of checkIn and checkOut. |
| `pricing` | `PartnerPricing` | yes | no |  |
| `pricing.mode` | `"indicative"` | yes | no | Quotes are indicative and revalidated by Hella. |
| `pricing.accommodation` | `DecimalMoney` | yes | no |  |
| `pricing.cleaningFee` | `DecimalMoney` | yes | no |  |
| `pricing.taxes` | `DecimalMoney` | yes | no |  |
| `pricing.total` | `DecimalMoney` | yes | no |  |
| `pricing.pricePreservedInHandoff` | `false` | yes | no | The price is not preserved and is recalculated during handoff. |
| `currency` | `Currency` | yes | no |  |
| `expiresAt` | `string` | yes | no | Expiry instant in UTC. Expiry does not make an indicative price guaranteed. |
| `handoffUrl` | `string` | yes | no | Absolute Hella URL containing one opaque signed handoff value. Use unchanged. |

## Quick Start

Run the generated `quickstart.mjs`. It creates future dates, searches, handles zero results, reads detail and availability, creates a quote, and uses the returned `handoffUrl`.

```sh
PARTNER_API_BASE_URL="https://www.app.hellastays.com/app/api" \
PARTNER_API_TOKEN="YOUR_PARTNER_TOKEN" \
node generated/partner-api/quickstart.mjs
```

For deterministic local execution, start `mock-server.mjs` and run the Quick Start with `PARTNER_API_BASE_URL=http://127.0.0.1:4010` and the synthetic fixture token `sandbox_partner_token`.

## Changelog

### 1.0.0-beta.1 — 2026-07-13

- Introduced the provider-neutral Partner API v1 contract.
- Standardised validation errors on RFC 9457 Problem Details.
- Added server-generated handoff URLs with signed partner attribution.
