> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gridbeds.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Read one property, or its tombstone

<Warning>**Preview.** This operation is not served yet: it is part of the released contract (version 1.0.0). Build against a mock of the contract until the sandbox serves it; see [Testing: mock and sandbox](../../testing).</Warning>


## OpenAPI

````yaml /openapi/distribution-v1.yaml get /properties/{property_id}
openapi: 3.1.0
info:
  title: Gridbeds Distribution API
  version: 1.0.0
  summary: >-
    Locations, owners, properties, availability and prices for distribution
    channels.
  description: >
    **Version 1.0.0**, agreed with listing partners over two reviews. An
    operation marked

    `x-gridbeds-availability: planned` is agreed and not served yet; it loses
    the mark when it is served, on the

    sandbox first. From this version v1 only grows: what a channel builds
    against it keeps working.


    **One file, one name.** This document is published as `distribution-v1.yaml`
    and keeps that name for all of v1,

    so two copies can be compared line by line. Which revision a copy is says
    `info.version`, and the contract

    changelog published beside it says what each revision changed.


    Pull for data, push for notifications. A webhook only says what changed;
    read the resource it names. Listing

    reports go the other way: you tell Gridbeds what became of each listing and
    how it performs, and the host sees

    it beside the bookings you send.


    **Ids** are opaque, prefixed, specific to your channel and never reused.

    **Instants** are RFC 3339 UTC seconds ending in `Z`. **Nights** are the
    property's local dates (`timezone`).

    **Money** is `{"amount": <integer ISO 4217 minor units>, "currency":
    "RON"}`. **Text** is a map of BCP 47

    language to plain text and always contains the property's source language:
    `ro` for a property in Romania, the

    property's own language elsewhere (`it` in Italy, `es` in Spain), where `ro`
    is not required. Other languages

    appear once the host has written them. The one exception is a text Gridbeds
    generates rather than the host

    writes, `cancellation.text`: it comes in the languages Gridbeds ships, so it
    is in `en` where the source language

    is not one of them.


    **Lists** return `{data, next_cursor}` (`null` at the end) and accept
    `limit` (1–500, default 100), `cursor`

    and `updated_since`. `updated_since` is inclusive: send back the largest
    `updated_at` you received — no overlap

    is needed — and apply a resource only when its `version` is higher than the
    one you hold. With `updated_since`,

    lists include withdrawn and deleted resources as tombstones `{id, status,
    version, updated_at}`. An

    `updated_since` older than the retention window (35 days) is answered 410
    `sync-window-expired`: resync from

    scratch.


    **One lifecycle for every resource.** Locations, owners, properties,
    amenities and webhook endpoints share one

    `status`, with the same meaning everywhere: `active` (published and in
    effect); `inactive` (published, not in

    effect now: a property that cannot be sold, a location or owner whose every
    published property is inactive, a

    suspended endpoint); `withdrawn` (not published to you now; it may come back
    under the same id with a higher

    version); `deleted` (final; the id is never used again). A `withdrawn` or
    `deleted` resource is a tombstone

    `{id, status, version, updated_at}` and nothing else, on every resource.
    Handle all four values on every

    resource, including a value that resource does not reach today. A property's
    withdrawal and return are

    `property.withdrawn` and `property.distributed`; a location or owner
    changing status (it follows the strongest

    status of the properties naming it) is a `location.updated` or
    `owner.updated` with a higher version, and a

    deletion is `<kind>.deleted`.


    **Every enum is open**: accept values you do not know. v1 only grows; a
    breaking change is `/v2`, announced at

    least 30 days ahead with `Deprecation` and `Sunset` headers.


    **Errors** are RFC 9457 problem details (`application/problem+json`); branch
    on `type` or `code`, never on

    `title`, which follows `Accept-Language`. Every authenticated response,
    refusals included, counts against your

    rate limit and carries the RateLimit fields. 405 and 503 are answered the
    same way on every path.


    **Transport**: HTTPS only, TLS 1.2 or later. A plain-HTTP request is
    refused, never redirected, because its

    bearer key has already crossed the network in clear; treat such a key as
    compromised and roll it.


    **Legal data follows the property's jurisdiction.** What the law asks of a
    listing is declared as data, per

    country and, where the law is regional, per region or municipality.
    `legal.identifiers` lists every legal

    identifier a listing carries, as `{scheme, value, valid_until, issued_by}`.
    A scheme name is lowercase, prefixed

    with the jurisdiction that defines it (`ro.classification`, `it.cin`,
    `eu.str_registration`, `es.ri`), and never

    renamed or reused. **Ignore a scheme you do not know**: schemes are added
    without a new version.

    `legal.classification` and `legal.registration_number` keep the meaning the
    specification gives them: a property

    in Romania is published only with a valid classification certificate and is
    withdrawn (`property.withdrawn`) when

    it expires; a property in Italy is published only with its CIN, as
    `registration_number`, and

    `classification: null`; elsewhere both are `null` unless a jurisdiction's
    rule fills them. Which countries you

    publish is your decision: filter on the location's `address.country`.


    **One property is one room type.** A guesthouse with three room types is
    three properties under one location,

    each with its `units`. The location's `name` and `category` let you show
    them as one listing of a hotel or a

    guesthouse with its room types, or each room type as a listing of its own:
    that choice is yours.


    **Addresses follow the host's choice.** A host decides per location whether
    its exact address is published.

    With `address_visibility: approximate` (the default when the owner is an
    individual, whose address is personal

    data) the location carries no street, number, building or postal code, and
    `geo` is a displaced point within

    `geo.precision_m` metres of the property. The host also chooses that radius:
    200, 500 or 1000 metres, 500 unless

    the host picks another (a dense city centre calls for less than a village).
    The point is fixed for each radius:

    the same on every read while the radius stays, and it cannot be turned back
    into the exact one. The exact

    address is never sent while the location is approximate. A change of
    visibility or of radius is a

    `location.updated` with a higher version, and a change of radius moves the
    point once; replace the address and

    point you hold, and when a location becomes approximate stop showing the
    exact ones you received before.


    **Prices follow the party.** Each night of a rate carries the price for
    every number of adults the property

    takes (`prices_by_occupancy`), what each child adds by age band
    (`child_prices`, the bands named on the rate

    plan) and what an infant adds, all as the guest pays them, so the price of
    any stay is a sum and you compute no

    rule. `price`, `base_occupancy` and `extra_guest_price` keep their meaning
    and are defined from the list.


    **The price of a stay** for a party on one rate plan is, for each night from
    arrival up to departure

    (exclusive): the night's `prices_by_occupancy` entry for the number of
    adults, plus for each child the

    `child_prices` entry of its age band (or `child_price` when the plan names
    no bands), plus `infant_price` for

    each infant; then each fee with `included_in_price: false` and `payable_at:
    booking`, added once for `per: stay`,

    once per night for `per: night` and once per guest and night for `per:
    guest_night` (in v1, the cleaning fee,

    once per stay). That sum is the price of the stay the guest books on the
    property's own site, VAT included

    (however much of it the rate plan takes as a deposit). Nothing else is added
    and nothing is taken off: Gridbeds

    applies no reduction for the length of a stay or for booking early or late.
    A cheaper rate for a longer or an

    earlier stay is a rate plan of its own, with its own nightly prices and
    rules (`min_stay`, `closed_to_arrival`):

    offer every plan whose rules admit the stay, as the property's own site
    does, and the cheapest of them is the

    lowest price Gridbeds offers for it.


    A fee with `payable_at: property` (in v1, the tourist tax, `city_tax`) is
    paid to the property during the stay

    and is not part of that price; the property's own site shows it beside the
    price, and so should you. A fee with

    `included_in_price: true` is already in the nightly prices: show it, and add
    nothing. Charges a property posts to

    a stay on its own account during it (parking, a resort fee, breakfast billed
    by the night) are not published in

    v1; they are not part of the price on the property's own site either, and
    are settled at the property.


    **The tourist tax is never left out.** Some municipalities levy it as a
    fixed amount per guest and night, others

    as a percentage of the accommodation price, and the amounts change by local
    decision. Every property says

    which of three things is true in `city_tax_status`: `levied` (its `fees`
    carry one `city_tax`), `none` (the

    property declares that no tourist tax applies) or `undeclared` (the property
    has not said; one may apply). A

    property without a `city_tax` fee is therefore never a statement that there
    is no tax unless `city_tax_status`

    is `none`. A `city_tax` fee states what is known and no more:


    - `basis: fixed` with `amount`: per taxable guest and night (`per:
    guest_night`). The tax of a stay is `amount` ×
      taxable guests × nights, the nights capped at `max_nights`, rounded once to the currency's minor unit; the
      taxable guests are the adults and the children, or the adults alone when `exempts_children` is true, and never
      the infants. That is the most the property charges: any further exemption it applies (a number of exempt
      guests per stay, for instance) only lowers it. `amount` is the rate in force on the property's today; a stay
      that begins after a new rate takes effect is charged the new one.
    - `basis: percent` with `percent_bp`: a percentage, in basis points (125 is
    1.25%), of the accommodation price
      named by `percent_of`, for the whole stay (`per: stay`). Gridbeds does not compute it and neither should you:
      show the percentage as payable at the property ("local tax 1.25%, paid at the property"). Exemptions from a
      percentage are not modelled: `exempts_children` is null and the property applies its own.
    - A value that is not known is null and the fee is still there: `basis:
    fixed` with `amount: null`, or
      `basis: percent` with `percent_bp: null`, says that the tax exists and how it is charged but not how much;
      `basis: null` (with `amount` and `percent_bp` null) says only that a tourist tax applies. Show such a fee as
      "local tax payable at the property" and never as zero.

    The property's own quote can differ from your sum in four ways, and in no
    other. **A promo code** lowers it for

    a guest who types one on the property's site; the rate plan's
    `lower_price_reasons` says when that can happen,

    and a code is never published. **Rounding**: Gridbeds rounds VAT once over
    the whole stay, so the sum can differ

    by up to one minor unit for each amount you added (each component of each
    night, and each fee), until `/quotes`

    (stage 2) answers the exact figure. **A change of VAT rate** during a stay:
    Gridbeds then charges every night at

    the rate in force on the departure date, while each published night carries
    the rate in force on the morning

    after it. **Your markup**: it applies to the nightly prices you receive and
    never to fees, and the property's

    site shows its prices without it. One more difference holds until the
    property's site asks for each child's age

    and for infants: the redirect carries only the number of adults and of
    children (`url_template`), so a party

    with children on a plan that prices them by age band is priced there without
    their ages, and a party's infants

    are not priced there at all.


    **Availability is published at least 365 nights ahead**, on a window that
    rolls with each property's local

    date: every night from the property's today up to `horizon_end` (exclusive)
    is present, and a night with no rate

    comes with every rate `closed`, never missing. `horizon_end` is at least 365
    nights after the property's today

    and may be further; it is in every availability response.


    **Build against a mock.** A mock server generated from this document answers
    every operation before it is

    served, e.g. `npx @stoplight/prism-cli mock --dynamic distribution-v1.yaml`
    (pin an exact 5.x release); `prism

    proxy` checks the sandbox's answers against the document. Every change is
    recorded in the contract changelog

    published beside it. Generated client libraries follow from the same
    document.


    **Two environments, configured apart.** The sandbox (`gbd_test_` keys) and
    production (`gbd_live_` keys) are

    separate deployments, each with its own record of your channel: keys,
    webhook endpoints and the owner-connect

    redirect URL are registered with each one separately, so the sandbox can
    point at your staging site while

    production points at your live one. Nothing registered in one environment
    exists in the other.


    **Testing webhooks.** A webhook endpoint must be a public HTTPS address: a
    private, loopback, link-local or

    reserved target is refused, so that a registered URL cannot be used to reach
    networks the internet cannot

    (server-side request forgery). A webhook therefore cannot reach a
    developer's machine. Locally, read

    `GET /events`, which carries the same events in commit order, or expose your
    receiver through a public HTTPS

    tunnel; webhooks themselves are exercised against the sandbox.
  contact:
    name: Gridbeds partner support
    email: support@gridbeds.com
  license:
    name: Proprietary
    url: https://gridbeds.com/en/terms/
servers:
  - url: https://api.gridbeds.com/api/v1/distribution
    description: Production (gbd_live_ keys)
  - url: https://staging-api.gridbeds.com/api/v1/distribution
    description: Sandbox (gbd_test_ keys)
security:
  - distributionKey: []
tags:
  - name: Channel
    description: The channel the key belongs to.
  - name: Catalog
    description: Locations, owners, properties and the amenity catalogue.
  - name: Availability
    description: Nightly availability, prices and restrictions.
  - name: Events
    description: The ordered log of what changed, and the webhook notification.
  - name: Webhooks
    description: The channel's webhook endpoints and their deliveries.
  - name: Listings
    description: >
      What the channel reports back about its listings: the publication state of
      each property on your side

      (`PUT /properties/{property_id}/listing`) and its daily statistics (`POST
      /listing-stats`), both with the

      scope `write:listings`. Gridbeds shows them to the host in its dashboard
      (views, clicks to booking, whether a

      listing is published and promoted) and joins them with the bookings that
      arrive from you. Gridbeds sends no

      webhook for them: it is the receiver.
  - name: Owner connect
    description: >
      A host links its Gridbeds listings to an account on your side, and
      Gridbeds shares no e-mail address or other

      contact detail to make it happen.


      1. The host starts "Connect your account" for your channel in Gridbeds.

      2. Gridbeds sends the host's browser to your redirect URL
      (`owner_connect.redirect_url` of `GET /channel`,
         registered with Gridbeds for each environment) as an HTML form POST carrying `token` (the `ownerConnect`
         webhook), so the token never appears in a URL, a server log, the browser history or a `Referer`.
      3. You verify the token, let the host sign in or sign up on your side (the
      host types their own e-mail address
         there if you need one), ask the signed-in host to confirm the link on your own page (showing the owner's
         `display_name`), bind the token's `sub`, an owner id you hold from `GET /owners`, to that account, and
         confirm with `PUT /owners/{owner_id}/connection`. The host then sees the account connected in Gridbeds.
         Never bind on the POST alone: any site can make a browser submit a form, so a token posted without the
         host's confirmation could link someone else's owner to the account that happens to be signed in.

      Nothing about the host reaches you except through that action.


      **The token** is a JWS in compact serialization (RFC 7515; a JWT, RFC
      7519) signed with ES256. Header:

      `alg` `ES256`, `typ` `owner-connect+jwt`, `kid` the signing key. Claims:
      `iss` the API's origin

      (`https://api.gridbeds.com`, or `https://staging-api.gridbeds.com` in the
      sandbox); `aud` your channel, the

      `channel` of `GET /channel`; `sub` the owner id; `property_ids` the ids of
      that owner's properties distributed

      to you when the host started; `iat`; `exp`, at most 300 seconds after
      `iat`; `jti`, a random single-use id

      (128 bits); `locale`, the host's interface language (BCP 47), which may be
      absent.


      **Verify all of it before trusting any claim:** the signature, with the
      key named by `kid` in

      `GET /owner-connect/keys` (a JWKS, RFC 7517; fetch it again on an unknown
      `kid` and cache it as its

      `Cache-Control` says); `alg` exactly `ES256` and `typ` exactly
      `owner-connect+jwt`, refusing `none` and every

      other value (RFC 8725); `iss` and `aud` as above; `exp` in the future and
      `iat` not in the future, with at most

      60 seconds of clock skew; `sub` an owner distributed to you. A new signing
      key is published at least 24 hours

      before it signs, and a retired one stays published 24 hours after it
      stops.


      **Two deadlines.** `exp` (at most 300 seconds) bounds the browser's leg:
      your redirect page accepts the token only

      before it. The confirmation may come later, because the host may have to
      sign up first: `PUT

      /owners/{owner_id}/connection` accepts an unconfirmed token until 3600
      seconds after its `iat`, and only from

      the channel named in its `aud`, with that channel's own key. Keep the
      verified token server-side meanwhile,

      never in a page or a URL.


      **Replay.** A token is single use. Keep every `jti` you accept until its
      `exp` and refuse one you have seen.

      Gridbeds binds a token to the one connection it confirms: confirming it
      again for the same owner answers the

      same connection, and any other use is refused.
paths:
  /properties/{property_id}:
    get:
      tags:
        - Catalog
      summary: Read one property, or its tombstone
      operationId: getProperty
      parameters:
        - $ref: '#/components/parameters/PropertyIdPath'
      responses:
        '200':
          description: The property or its tombstone.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Request-Id:
              $ref: '#/components/headers/RequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimitPolicy'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Property'
        '304':
          $ref: '#/components/responses/NotModified'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
      security:
        - distributionKey:
            - read:catalog
components:
  parameters:
    PropertyIdPath:
      name: property_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/PropertyId'
  headers:
    ETag:
      schema:
        type: string
      description: Weak validator; send it back as If-None-Match for a 304.
    RequestId:
      schema:
        type: string
        format: uuid
      description: Quote it to support; equals the problem's instance.
    RateLimitLimit:
      schema:
        type: integer
    RateLimitRemaining:
      schema:
        type: integer
    RateLimitReset:
      schema:
        type: integer
      description: Seconds until the window resets.
    RateLimitPolicy:
      schema:
        type: string
      description: >-
        draft-ietf-httpapi-ratelimit-headers, e.g. "partner";q=600;w=60
        (provisional; the three RateLimit-* fields above are the stable
        contract).
    RateLimit:
      schema:
        type: string
      description: draft-ietf-httpapi-ratelimit-headers, e.g. "partner";r=598;t=37
    WWWAuthenticate:
      schema:
        type: string
      description: RFC 6750, e.g. Bearer realm="distribution", error="invalid_token"
    RetryAfter:
      schema:
        type: integer
      description: Seconds.
  schemas:
    Property:
      oneOf:
        - $ref: '#/components/schemas/PropertyRecord'
        - $ref: '#/components/schemas/Tombstone'
      discriminator:
        propertyName: status
        mapping:
          active: '#/components/schemas/PropertyRecord'
          inactive: '#/components/schemas/PropertyRecord'
          withdrawn: '#/components/schemas/Tombstone'
          deleted: '#/components/schemas/Tombstone'
    PropertyId:
      type: string
      pattern: ^prp_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - prp_1qnykkykp69yv8d59hxk5410m9
    PropertyRecord:
      type: object
      required:
        - id
        - status
        - location_id
        - owner_id
        - type
        - units
        - name
        - description
        - capacity
        - photos
        - check_in
        - check_out
        - rate_plans
        - fees
        - city_tax_status
        - legal
        - booking
        - timezone
        - version
        - updated_at
      properties:
        id:
          $ref: '#/components/schemas/PropertyId'
        status:
          type: string
          enum:
            - active
            - inactive
          description: inactive while the property cannot be sold.
        location_id:
          $ref: '#/components/schemas/LocationId'
        owner_id:
          $ref: '#/components/schemas/OwnerId'
        type:
          type: string
          enum:
            - apartment
            - studio
            - room
            - house
            - villa
            - cabin
            - guesthouse_room
            - hotel_room
            - other
        units:
          type: integer
          minimum: 1
          default: 1
          description: Identical units sold under this property.
        name:
          $ref: '#/components/schemas/LocalizedText'
        description:
          $ref: '#/components/schemas/LocalizedText'
        capacity:
          $ref: '#/components/schemas/Capacity'
        area_sqm:
          type:
            - integer
            - 'null'
        floor:
          type:
            - integer
            - 'null'
        elevator:
          type:
            - boolean
            - 'null'
        amenities:
          type: array
          items:
            type: string
          description: Codes from /amenities.
        photos:
          type: array
          minItems: 5
          items:
            $ref: '#/components/schemas/Photo'
        check_in:
          type: object
          required:
            - from
          properties:
            from:
              $ref: '#/components/schemas/LocalTime'
            until:
              oneOf:
                - $ref: '#/components/schemas/LocalTime'
                - type: 'null'
        check_out:
          type: object
          required:
            - until
          properties:
            until:
              $ref: '#/components/schemas/LocalTime'
        house_rules:
          $ref: '#/components/schemas/HouseRules'
        rate_plans:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/RatePlan'
        fees:
          type: array
          items:
            $ref: '#/components/schemas/Fee'
          description: >-
            v1 publishes cleaning and city_tax, each at most once; city_tax
            exactly when city_tax_status is levied.
          examples:
            - - type: cleaning
                basis: fixed
                amount:
                  amount: 15000
                  currency: RON
                percent_bp: null
                percent_of: null
                per: stay
                included_in_price: false
                payable_at: booking
                max_nights: null
                exempts_children: null
              - type: city_tax
                basis: percent
                amount: null
                percent_bp: 125
                percent_of: accommodation_excluding_vat
                per: stay
                included_in_price: false
                payable_at: property
                max_nights: null
                exempts_children: null
        city_tax_status:
          type: string
          enum:
            - levied
            - none
            - undeclared
          examples:
            - levied
          description: >
            Additive. Whether a tourist tax applies to a stay here, so a missing
            `city_tax` fee is never mistaken for

            "no tax". levied: `fees` carries exactly one `city_tax`, whose value
            may still be unknown (null). none: the

            property declares that no tourist tax applies; `fees` carries no
            `city_tax`. undeclared: the property has not

            said whether one applies; `fees` carries no `city_tax`, and you
            should say that a local tax may be payable

            at the property. A change is a `property.updated` with a higher
            version. Open enum: read a value you do not

            know as undeclared.
        legal:
          $ref: '#/components/schemas/Legal'
        booking:
          $ref: '#/components/schemas/BookingMethod'
        extensions:
          type: object
          additionalProperties:
            type: object
          description: Only your own channel key appears.
        timezone:
          $ref: '#/components/schemas/TimeZone'
        version:
          $ref: '#/components/schemas/Version'
        updated_at:
          $ref: '#/components/schemas/Instant'
    Tombstone:
      type: object
      required:
        - id
        - status
        - version
        - updated_at
      additionalProperties: false
      description: A withdrawn or deleted resource of any kind. Nothing else is sent.
      properties:
        id:
          type: string
          description: The resource's id (an amenity's code).
        status:
          type: string
          enum:
            - withdrawn
            - deleted
        version:
          $ref: '#/components/schemas/Version'
        updated_at:
          $ref: '#/components/schemas/Instant'
    Problem:
      type: object
      required:
        - type
        - title
        - status
        - code
        - instance
        - request_id
      additionalProperties: true
      properties:
        type:
          type: string
          format: uri
          description: >-
            https://api.gridbeds.com/problems/{slug}; resolves to a page
            describing the problem.
          examples:
            - https://api.gridbeds.com/problems/not-found
          x-gridbeds-slugs:
            - invalid-parameters
            - invalid-cursor
            - window-beyond-horizon
            - unauthorized
            - insufficient-scope
            - partner-suspended
            - not-found
            - method-not-allowed
            - conflict
            - sync-window-expired
            - event-cursor-expired
            - unsupported-media-type
            - rate-limited
            - internal
            - service-unavailable
        title:
          type: string
        status:
          type: integer
          minimum: 400
          maximum: 599
        detail:
          type: string
        instance:
          type: string
          format: uri
          description: urn:uuid:<Request-Id>
        code:
          $ref: '#/components/schemas/ProblemCode'
        request_id:
          type: string
          format: uuid
    ScopeProblem:
      allOf:
        - $ref: '#/components/schemas/Problem'
        - type: object
          required:
            - required_scopes
          properties:
            required_scopes:
              type: array
              items:
                $ref: '#/components/schemas/Scope'
    LocationId:
      type: string
      pattern: ^loc_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - loc_5ckd3q6vsn9qmt7xrj0689h226
    OwnerId:
      type: string
      pattern: ^own_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - own_63g5d4zsqj809bf5rwekga54a5
    LocalizedText:
      type: object
      minProperties: 1
      propertyNames:
        $ref: '#/components/schemas/LanguageTag'
      additionalProperties:
        type: string
        minLength: 1
      description: >-
        Plain text by language; the property's source language is always present
        (ro for a property in Romania, its own language elsewhere), except in a
        text Gridbeds generates (cancellation.text), which is in en where it
        does not ship the source language. No HTML, links, e-mail addresses or
        phone numbers.
    Capacity:
      type: object
      required:
        - max_guests
        - max_adults
        - max_children
        - beds
      properties:
        max_guests:
          type: integer
          minimum: 1
        max_adults:
          type: integer
          minimum: 1
        max_children:
          type: integer
          minimum: 0
        max_infants:
          type: integer
          minimum: 0
          description: >-
            Additive. Infants taken beside max_guests (in a cot, using no bed);
            priced by infant_price.
        bedrooms:
          type:
            - integer
            - 'null'
        bathrooms:
          type:
            - integer
            - 'null'
        beds:
          type: array
          items:
            type: object
            required:
              - type
              - count
            properties:
              type:
                type: string
                enum:
                  - single
                  - double
                  - queen
                  - king
                  - sofa_bed
                  - bunk
              count:
                type: integer
                minimum: 1
    Photo:
      type: object
      required:
        - id
        - url
        - content_type
        - width
        - height
        - order
        - checksum
      properties:
        id:
          $ref: '#/components/schemas/PhotoId'
        url:
          type: string
          format: uri
          description: Public HTTPS, never expires. A replaced file gets a new id.
        content_type:
          type: string
          enum:
            - image/jpeg
            - image/webp
        width:
          type: integer
          minimum: 1
        height:
          type: integer
          minimum: 1
          description: The long side (width or height) is at least 1600 px.
        order:
          type: integer
          minimum: 0
          description: 0 is the main photo.
        room:
          type:
            - string
            - 'null'
          enum:
            - living
            - bedroom
            - kitchen
            - bathroom
            - exterior
            - view
            - other
            - null
        caption:
          oneOf:
            - $ref: '#/components/schemas/LocalizedText'
            - type: 'null'
        checksum:
          type: string
          pattern: ^[0-9a-f]{64}$
          description: Lowercase hex SHA-256 of the bytes at url.
    LocalTime:
      type: string
      pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
      examples:
        - '14:00'
    HouseRules:
      type: object
      properties:
        smoking:
          type:
            - boolean
            - 'null'
        pets:
          type:
            - boolean
            - 'null'
        parties:
          type:
            - boolean
            - 'null'
        quiet_hours:
          oneOf:
            - type: object
              required:
                - from
                - until
              properties:
                from:
                  $ref: '#/components/schemas/LocalTime'
                until:
                  $ref: '#/components/schemas/LocalTime'
            - type: 'null'
        min_age:
          type:
            - integer
            - 'null'
    RatePlan:
      type: object
      required:
        - id
        - name
        - meal_plan
        - cancellation
        - currency
        - lower_price_reasons
      description: >
        A way to sell the property, with its own nightly prices and rules.
        Gridbeds applies no automatic reduction to

        a plan's prices for the length of a stay or for booking early or late: a
        cheaper rate for a longer or an

        earlier stay is a plan of its own (its `min_stay` and
        `closed_to_arrival` on each night say which stays it

        admits), so the cheapest plan whose rules admit a stay gives the lowest
        price Gridbeds offers for it.
      properties:
        id:
          $ref: '#/components/schemas/RatePlanId'
        name:
          $ref: '#/components/schemas/LocalizedText'
        meal_plan:
          type: string
          enum:
            - none
            - breakfast
            - half_board
            - full_board
            - all_inclusive
          description: Open enum.
        cancellation:
          $ref: '#/components/schemas/Cancellation'
        currency:
          $ref: '#/components/schemas/CurrencyCode'
        child_age_bands:
          type: array
          items:
            $ref: '#/components/schemas/ChildAgeBand'
          description: >
            Additive. The age bands this plan prices children by, in ascending
            order and never overlapping; the price of

            each is on every night (`child_prices`). A child whose age falls in
            no band cannot be booked. Empty when the

            property prices children without their age, by each night's
            `child_price`. Ask the guest for each child's

            age when it is not empty.
        lower_price_reasons:
          type: array
          uniqueItems: true
          items:
            $ref: '#/components/schemas/LowerPriceReason'
          examples:
            - []
            - - promo_code
          description: >
            Additive. Why a guest may pay less than the price of a stay summed
            from this plan's nights and fees (see the

            description of this document), so you can say that the final price
            may be lower. Empty when none applies;

            the other differences that description lists (rounding, a change of
            VAT rate, a party's children and

            infants) are not reasons here. A change to it is a
            `property.updated` with a higher version.
    Fee:
      type: object
      required:
        - type
        - basis
        - amount
        - percent_bp
        - percent_of
        - per
        - included_in_price
        - payable_at
        - max_nights
        - exempts_children
      description: >
        A charge beside the nightly prices. In v1: `cleaning` (a fixed amount
        per stay, VAT included, payable at

        booking, part of the price of a stay) and `city_tax` (the tourist tax,
        payable at the property, never part of

        it: a fixed amount per guest and night or a percentage of the
        accommodation price, see `basis`). Neither

        carries your markup. Every member is always present; one that does not
        apply is null.


        What v1 sends, by type and basis:


        | type | basis | amount | percent_bp | percent_of | per | max_nights |
        exempts_children |

        | --- | --- | --- | --- | --- | --- | --- | --- |

        | cleaning | fixed | Money | null | null | stay | null | null |

        | city_tax | fixed | Money or null | null | null | guest_night | integer
        or null | boolean or null |

        | city_tax | percent | null | integer or null | value or null | stay |
        integer or null | null |

        | city_tax | null | null | null | null | null | null | null |
      examples:
        - type: cleaning
          basis: fixed
          amount:
            amount: 15000
            currency: RON
          percent_bp: null
          percent_of: null
          per: stay
          included_in_price: false
          payable_at: booking
          max_nights: null
          exempts_children: null
        - type: city_tax
          basis: percent
          amount: null
          percent_bp: 125
          percent_of: accommodation_excluding_vat
          per: stay
          included_in_price: false
          payable_at: property
          max_nights: null
          exempts_children: null
        - type: city_tax
          basis: fixed
          amount:
            amount: 500
            currency: RON
          percent_bp: null
          percent_of: null
          per: guest_night
          included_in_price: false
          payable_at: property
          max_nights: 7
          exempts_children: true
        - type: city_tax
          basis: null
          amount: null
          percent_bp: null
          percent_of: null
          per: null
          included_in_price: false
          payable_at: property
          max_nights: null
          exempts_children: null
      properties:
        type:
          type: string
          enum:
            - cleaning
            - city_tax
            - extra_guest
            - pet
          description: Open enum; v1 publishes cleaning and city_tax.
        basis:
          type:
            - string
            - 'null'
          enum:
            - fixed
            - percent
            - null
          examples:
            - fixed
            - percent
          description: >
            Additive. How the fee is charged. fixed: `amount` (per `per`).
            percent: `percent_bp` of the price named by

            `percent_of`; city_tax only. null: city_tax only, when a tourist tax
            applies and how it is charged is not

            known; `amount` and `percent_bp` are then null too. Open enum: show
            a fee whose basis you do not know as

            "payable at the property" when `payable_at` is property, and do not
            add it to the price of a stay.
        amount:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
          description: >
            For basis fixed, the amount per `per`. null when basis is percent or
            null, and for a city_tax of basis

            fixed whose amount is not known: the tax exists and is not
            published, so show it as payable at the

            property, never as zero. A cleaning fee always has one.
        percent_bp:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 10000
          examples:
            - 125
          description: >
            Additive. For basis percent, the rate in basis points: 125 is 1.25%.
            An integer, never a fraction, so a rate

            is never rounded on the way. null for any other basis, and for a
            percentage whose rate is not known.
        percent_of:
          type:
            - string
            - 'null'
          enum:
            - accommodation_excluding_vat
            - accommodation_including_vat
            - null
          examples:
            - accommodation_excluding_vat
          description: >
            Additive. For basis percent, the price the percentage is of: the
            accommodation price of the stay at the

            property's own rates (without your markup and without fees),
            excluding or including VAT. The prices

            published here include VAT and your markup, so the base cannot be
            derived from them exactly; show the

            percentage, do not compute an amount. null for any other basis, and
            when the base is not known. Open enum:

            show the percentage and read a base you do not know as "the
            accommodation price".
        per:
          type:
            - string
            - 'null'
          enum:
            - stay
            - night
            - guest_night
            - null
          description: >
            stay: once per stay. night: once per night. guest_night: once per
            guest and night (for city_tax, per taxable

            guest and night, see exempts_children and max_nights). A city_tax of
            basis percent is always per stay: the

            percentage is of the accommodation price of the whole stay (a
            percentage of each night summed is the same

            percentage of the stay), so night and guest_night never go with
            percent. null only when basis is null.
        included_in_price:
          type: boolean
          description: 'true: already in the nightly prices; show it and add nothing.'
        payable_at:
          type: string
          enum:
            - property
            - booking
          description: >-
            Additive; always present. booking: part of the price of a stay and
            paid however the booking is paid. property: paid to the property
            during the stay, not part of the price of a stay; show it beside the
            price.
        max_nights:
          type:
            - integer
            - 'null'
          minimum: 1
          description: >
            city_tax only: the nights are counted up to this many; null when
            every night is. For basis fixed, the

            nights multiplied by `amount`. For basis percent, the base is the
            accommodation price of the first this-many

            nights of the stay.
        exempts_children:
          type:
            - boolean
            - 'null'
          description: >
            city_tax of basis fixed only: true when only adults are taxed, false
            when children are taxed too; infants are

            never taxed. null when not known, and always null for basis percent:
            exemptions from a percentage are not

            modelled in v1, and the property applies its own when it charges the
            tax.
    Legal:
      type: object
      required:
        - classification
        - registration_number
        - identifiers
      description: What the law of the property's jurisdiction asks a listing to show.
      properties:
        classification:
          oneOf:
            - $ref: '#/components/schemas/Classification'
            - type: 'null'
          description: >-
            Present on every published property in Romania (a property whose
            certificate expires is withdrawn); null in every other country. The
            same certificate is also listed in identifiers as ro.classification.
        registration_number:
          type:
            - string
            - 'null'
          description: >-
            The national registration number: in Italy the CIN; under Regulation
            (EU) 2024/1028 elsewhere once the national registry exists;
            otherwise null. Always also listed in identifiers.
        identifiers:
          type: array
          items:
            $ref: '#/components/schemas/LegalIdentifier'
          description: >-
            Additive. Every legal identifier the listing carries, at most one
            per scheme; empty where the jurisdiction asks for none. Ignore a
            scheme you do not know.
    BookingMethod:
      type: object
      required:
        - method
      properties:
        method:
          type: string
          enum:
            - redirect
            - request
        url:
          type:
            - string
            - 'null'
          format: uri
        url_template:
          type:
            - string
            - 'null'
          description: RFC 6570, e.g. …?room_type_id=…{&checkin,checkout,adults,children}
        request_email:
          type:
            - string
            - 'null'
          format: email
          description: A verified booking address, never a personal one.
    TimeZone:
      type: string
      examples:
        - Europe/Bucharest
        - Europe/Rome
      description: IANA time zone.
    Version:
      type: integer
      format: int64
      minimum: 1
      description: >-
        Grows on every change visible to this channel; ignore a lower one. Not a
        counter — do not expect consecutive values.
    Instant:
      type: string
      format: date-time
      pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$
      examples:
        - '2026-10-16T09:30:00Z'
    ProblemCode:
      type: string
      description: Open enum. Branch on this or on type.
      enum:
        - invalid_parameters
        - invalid_cursor
        - window_beyond_horizon
        - missing_api_key
        - invalid_api_key
        - api_key_wrong_environment
        - api_key_expired
        - api_key_revoked
        - insufficient_scope
        - partner_suspended
        - not_found
        - method_not_allowed
        - webhook_endpoint_exists
        - webhook_endpoint_limit_reached
        - webhooks_suspended_by_platform
        - webhook_endpoint_still_failing
        - sync_window_expired
        - event_cursor_expired
        - unsupported_media_type
        - rate_limited
        - too_many_failed_authentications
        - internal
        - service_unavailable
    Scope:
      type: string
      enum:
        - read:catalog
        - read:ari
        - manage:webhooks
        - write:listings
      description: >-
        Open enum. write:listings covers what you report back to Gridbeds,
        listing reports and owner connections. write:bookings is reserved.
    PhotoId:
      type: string
      pattern: ^pho_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - pho_5v2a9ddapw9h2a8rb96b289923
    RatePlanId:
      type: string
      pattern: ^rpl_[0-7][0-9a-hjkmnp-tv-z]{25}$
      examples:
        - rpl_3xdccbsf4p8akbsma1qwypy8pc
    Cancellation:
      type: object
      required:
        - refundable
        - free_until_days_before
        - penalty
        - penalty_percent
        - text
      properties:
        refundable:
          type: boolean
        free_until_days_before:
          type:
            - integer
            - 'null'
          minimum: 0
          description: null when non-refundable.
        penalty:
          type: string
          enum:
            - first_night
            - percent
            - full
          description: Open enum; v1 publishes full.
        penalty_percent:
          type:
            - integer
            - 'null'
          minimum: 0
          maximum: 100
        text:
          $ref: '#/components/schemas/LocalizedText'
          description: >-
            Generated by Gridbeds from the fields above, in the languages it
            ships (en, ro, it): in the source language when it is one of them,
            else in en (a property in Spain today). The fields above are what
            binds.
    CurrencyCode:
      type: string
      pattern: ^[A-Z]{3}$
      description: ISO 4217.
    ChildAgeBand:
      type: object
      required:
        - age_from
        - age_to
      properties:
        age_from:
          type: integer
          minimum: 0
          maximum: 17
          description: Inclusive age in years.
        age_to:
          type: integer
          minimum: 0
          maximum: 17
          description: Inclusive age in years.
    LowerPriceReason:
      type: string
      enum:
        - promo_code
      description: >
        Open enum; accept a value you do not know and read it as "the final
        price may be lower". promo_code: the

        property has a promo code a guest can still redeem on the property's own
        site, which takes a percentage or an

        amount off the whole stay (fees and VAT included, never below zero) for
        a guest who types it there, one code

        per stay. Codes are private to the property: which codes exist, what
        they are worth and their conditions are

        never published, and no code can be applied through this API. Listed
        only when `booking.method` is

        redirect, because only a guest sent to the property's site can type one.
    Money:
      type: object
      required:
        - amount
        - currency
      properties:
        amount:
          type: integer
          description: ISO 4217 minor units (45000 RON = 450.00 lei).
        currency:
          $ref: '#/components/schemas/CurrencyCode'
    Classification:
      type: object
      required:
        - certificate_number
        - category
        - rating_kind
        - rating
        - valid_until
        - country
      properties:
        certificate_number:
          type: string
        category:
          type: string
          description: >-
            Open enum of establishment categories (e.g. hotel,
            pensiune_agroturistica).
        rating_kind:
          type: string
          enum:
            - stars
            - flowers
        rating:
          type: integer
          minimum: 1
          maximum: 5
        valid_until:
          type: string
          format: date
        country:
          $ref: '#/components/schemas/CountryCode'
    LegalIdentifier:
      type: object
      required:
        - scheme
        - value
      properties:
        scheme:
          $ref: '#/components/schemas/LegalIdentifierScheme'
        value:
          type: string
          minLength: 1
          maxLength: 64
          description: The identifier exactly as the issuing authority wrote it.
        valid_until:
          type:
            - string
            - 'null'
          format: date
          description: >-
            Last day it is valid, in the property's timezone; absent or null
            when it does not expire.
        issued_by:
          type:
            - string
            - 'null'
          maxLength: 160
          description: >-
            The issuing authority, as text; for a regional scheme, which region
            issued it.
    CountryCode:
      type: string
      pattern: ^[A-Z]{2}$
      description: ISO 3166-1 alpha-2.
    LegalIdentifierScheme:
      type: string
      pattern: ^[a-z]{2}\.[a-z][a-z0-9_]*$
      maxLength: 48
      examples:
        - ro.classification
        - it.cin
        - eu.str_registration
        - es.ri
      description: >
        Open set. Lowercase `<jurisdiction>.<name>`: the jurisdiction is the ISO
        3166-1 alpha-2 code of the country

        that defines the scheme, or `eu` for a European Union scheme; a regional
        scheme carries its country's prefix

        and names its region in `issued_by`. A name is stable: never renamed or
        reused; a scheme whose format changes

        becomes a new name. Known in v1: `ro.classification` (Romanian tourism
        classification certificate number),

        `it.cin` (Italian Codice Identificativo Nazionale),
        `eu.str_registration` (registration number under

        Regulation (EU) 2024/1028, applicable from 20 May 2026 wherever a member
        state runs a registration

        procedure), `es.ri` (a Spanish regional tourism register number). Ignore
        a scheme you do not know.
  responses:
    NotModified:
      description: Unchanged since the ETag you sent.
      headers:
        ETag:
          $ref: '#/components/headers/ETag'
        Request-Id:
          $ref: '#/components/headers/RequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
    Unauthorized:
      description: >-
        unauthorized. code is missing_api_key, invalid_api_key (malformed or
        unknown — identical), api_key_wrong_environment (from the key's own
        prefix), api_key_expired or api_key_revoked (told apart only for a key
        whose secret matched).
      headers:
        WWW-Authenticate:
          $ref: '#/components/headers/WWWAuthenticate'
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Forbidden:
      description: >-
        insufficient-scope (with required_scopes) or partner-suspended. Counts
        against the rate limit.
      headers:
        WWW-Authenticate:
          $ref: '#/components/headers/WWWAuthenticate'
        Request-Id:
          $ref: '#/components/headers/RequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
      content:
        application/problem+json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/ScopeProblem'
              - $ref: '#/components/schemas/Problem'
    NotFound:
      description: >-
        not-found: does not exist, was never distributed to you, belongs to
        another channel, is malformed, or (availability) is withdrawn or
        deleted. Identical in every case.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    RateLimited:
      description: >-
        rate-limited (your budget, or repeated failed authentication from one
        address).
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        Request-Id:
          $ref: '#/components/headers/RequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimitPolicy'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Internal:
      description: internal.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  securitySchemes:
    distributionKey:
      type: http
      scheme: bearer
      bearerFormat: gbd_live_… or gbd_test_… (63 characters)
      description: >
        A key starts with `gbd_live_` (production) or `gbd_test_` (sandbox) and
        ends in a CRC32 checksum of its

        secret part, so a mistyped or truncated key is refused before any
        lookup, as is a key of the other

        environment. The prefix is registered with public secret scanners
        (GitHub secret scanning, TruffleHog,

        GitGuardian): a key found in a public repository is revoked at once and
        your technical contact is told.

        Gridbeds stores only a hash of each key. One partner may hold two keys
        per environment during a rotation.

        Operations list the scopes they need; the scopes are OAuth 2.0 scope
        tokens, so short-lived access tokens

        from a client-credentials grant can be offered later beside keys, with
        the same scopes and no change to any

        operation.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.