# GENERATED by scripts/build-openapi.ts from openapi/tatallo-pms-v1.yaml and
# openapi/examples/*.json. Do not edit: change the source, then run `pnpm openapi:build`.
openapi: 3.1.0
info:
  title: Tatallo PMS API
  version: 1.0.0
  summary: The server-to-server API a PMS uses to list and read the Tatallo properties its hosts have connected.
  description: |
    The Tatallo PMS API is a small, read-only, server-to-server API. A PMS calls it with a
    Tatallo-issued PMS credential and sees exactly the properties its hosts have connected.

    - Every response carries an `X-Request-Id` header, always a UUID. Send your own UUID in
      `X-Request-Id` to have it echoed back.
    - Every error has the shape `{ "error": { "code", "message", "request_id" } }`. Messages
      are English and may change; codes never do.
    - Each PMS credential may make 60 requests per minute (`GET /v1/health`: 60 per minute
      per client IP). Over the limit you get `429` with `Retry-After`.
    - Within `/v1` the contract changes only additively. A breaking change gets at least
      6 months' notice on the changelog.
  contact:
    name: Tatallo developer support
    email: hello@tatallo.com
servers:
  - url: https://api.tatallo.com
    description: Production
  - url: https://staging.api.tatallo.com
    description: Staging (synthetic properties only)
tags:
  - name: System
    description: Connectivity and the identity of your PMS credential.
  - name: Properties
    description: The Tatallo properties your hosts have connected to your PMS.
security:
  - PmsCredential: []
paths:
  /v1/health:
    get:
      operationId: getHealth
      tags:
        - System
      summary: Check that Tatallo is reachable
      description: |
        Answers `200` whenever the Tatallo PMS API is up. Needs no key, so it tells a network
        problem from an authentication problem. Rate-limited per client IP.
      security: []
      responses:
        "200":
          description: Tatallo is reachable.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
              examples:
                ok:
                  summary: Tatallo is reachable
                  value:
                    status: ok
        "429":
          $ref: "#/components/responses/RateLimited"
  /v1/me:
    get:
      operationId: getMe
      tags:
        - System
      summary: See which PMS credential you are using
      description: |
        Names the PMS credential that made the call: its public ID, your PMS, the environment
        it belongs to and its scopes. Any valid key may call it, whatever its scopes. It never
        returns a secret, so logging the response is safe.
      responses:
        "200":
          description: The calling PMS credential.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
              examples:
                staging:
                  summary: The staging Test PMS credential
                  value:
                    credential_id: 5d4e040b-61c1-44a4-a4ca-9a751dc05133
                    pms_name: Test PMS
                    environment: staging
                    scopes:
                      - properties:read
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /v1/properties:
    get:
      operationId: listProperties
      tags:
        - Properties
      summary: List the properties connected to your PMS
      description: |
        Lists every property your hosts have connected to your PMS, a page at a time, in a
        stable order. Follow `page.next_cursor` until it is `null` to see them all; each
        property appears exactly once. Needs the `properties:read` scope.
      parameters:
        - name: limit
          in: query
          required: false
          description: How many properties per page, from 1 to 100.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          required: false
          description: |
            The `page.next_cursor` of the previous page, exactly as returned. It is opaque and
            works only with the PMS credential and environment it was issued to. Don't store
            it or build it by hand.
          schema:
            type: string
      responses:
        "200":
          description: One page of properties.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PropertyList"
              examples:
                allOnOnePage:
                  summary: Every connected property on one page
                  value:
                    data:
                      - id: 6cfa992d-1805-4de6-892c-23785031aba0
                        name: Fixture Loft Kreuzberg
                        timezone: Europe/Berlin
                        status: active
                        country: Germany
                        city: Berlin
                        max_guests: 2
                        currency: EUR
                        updated_at: 2026-10-01T09:00:00.000Z
                      - id: 934f807c-39d6-45de-b107-d5c9e03cb041
                        name: Fixture House Alfama
                        timezone: Europe/Lisbon
                        status: inactive
                        country: Portugal
                        city: Lisbon
                        max_guests: 6
                        currency: EUR
                        updated_at: 2026-10-01T09:00:00.000Z
                      - id: bef2859f-2444-47ec-b5f4-f1e6701f087f
                        name: Fixture Apartment Trastevere
                        timezone: Europe/Rome
                        status: active
                        country: Italy
                        city: Rome
                        max_guests: 4
                        currency: EUR
                        updated_at: 2026-10-01T09:00:00.000Z
                    page:
                      next_cursor: null
                firstOfSeveralPages:
                  summary: "The first page at limit=1: follow page.next_cursor for the next"
                  value:
                    data:
                      - id: 6cfa992d-1805-4de6-892c-23785031aba0
                        name: Fixture Loft Kreuzberg
                        timezone: Europe/Berlin
                        status: active
                        country: Germany
                        city: Berlin
                        max_guests: 2
                        currency: EUR
                        updated_at: 2026-10-01T09:00:00.000Z
                    page:
                      next_cursor: pKZAA9cXZioUAmmXQVzN4RBwg8cC3KXflLg_Ys2qB0vNw7TfJ1kq3m8cWl5yHh0eE4tR6uD1aPo
        "400":
          description: "`limit` is out of range (`invalid_limit`), or `cursor` is not a cursor this key was given (`invalid_cursor`)."
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvalidListQueryError"
              examples:
                invalidLimit:
                  summary: limit is not a whole number from 1 to 100
                  value:
                    error:
                      code: invalid_limit
                      message: limit must be a whole number from 1 to 100.
                      request_id: 451fe2aa-a20b-4a92-b34f-3a27f5dd2f8b
                invalidCursor:
                  summary: The cursor was edited, made by hand or issued to another key
                  value:
                    error:
                      code: invalid_cursor
                      message: The cursor is not valid. Use page.next_cursor exactly as returned, with the same key.
                      request_id: 30c04c3a-c527-432c-8db3-f4943d4bcf57
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/InsufficientScope"
        "429":
          $ref: "#/components/responses/RateLimited"
  /v1/properties/{propertyId}:
    get:
      operationId: getProperty
      tags:
        - Properties
      summary: Read one property connected to your PMS
      description: |
        Reads one property your hosts have connected to your PMS. A property that is not
        connected to your PMS, was removed, or never existed gets the same `404`, so the
        answer reveals nothing about properties not connected to you. Needs the
        `properties:read` scope.
      parameters:
        - name: propertyId
          in: path
          required: true
          description: The property's `id`, as listed by `GET /v1/properties`.
          schema:
            type: string
            format: uuid
          example: bef2859f-2444-47ec-b5f4-f1e6701f087f
      responses:
        "200":
          description: The property.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Property"
              examples:
                active:
                  summary: A connected, published property
                  value:
                    id: bef2859f-2444-47ec-b5f4-f1e6701f087f
                    name: Fixture Apartment Trastevere
                    timezone: Europe/Rome
                    status: active
                    country: Italy
                    city: Rome
                    max_guests: 4
                    currency: EUR
                    updated_at: 2026-10-01T09:00:00.000Z
                inactive:
                  summary: A connected, suspended property
                  value:
                    id: 934f807c-39d6-45de-b107-d5c9e03cb041
                    name: Fixture House Alfama
                    timezone: Europe/Lisbon
                    status: inactive
                    country: Portugal
                    city: Lisbon
                    max_guests: 6
                    currency: EUR
                    updated_at: 2026-10-01T09:00:00.000Z
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/InsufficientScope"
        "404":
          description: The property is not connected to your PMS, or does not exist.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PropertyNotFoundError"
              examples:
                notFound:
                  summary: Not connected to your PMS, or does not exist
                  value:
                    error:
                      code: property_not_found
                      message: Property not found.
                      request_id: 385c1118-7082-4e15-97a7-16703f74d58b
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    PmsCredential:
      type: http
      scheme: bearer
      description: |
        A PMS credential issued by Tatallo: an opaque key of the form `tat_<env>_sk_…`, where
        `<env>` is `stg` on staging and `live` on production. A key works only in its own
        environment. Send it as `Authorization: Bearer <key>`, from your servers only.
  headers:
    XRequestId:
      description: The request's ID, on every response. Your own UUID if you sent a well-formed one in `X-Request-Id`, otherwise one Tatallo generated. Quote it to support.
      required: true
      schema:
        type: string
        format: uuid
    RetryAfter:
      description: Seconds to wait before the next request.
      required: true
      schema:
        type: integer
        minimum: 1
  responses:
    Unauthorized:
      description: The key is missing, malformed, for another environment, expired or revoked. All of these get the same answer.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/UnauthorizedError"
          examples:
            unauthorized:
              summary: Missing, malformed, expired or revoked key
              value:
                error:
                  code: unauthorized
                  message: Missing or invalid PMS credential.
                  request_id: e867476c-9237-4ebd-af42-b93abc47d0ed
    InsufficientScope:
      description: The key is valid but lacks the `properties:read` scope.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/InsufficientScopeError"
          examples:
            insufficientScope:
              summary: The key lacks properties:read
              value:
                error:
                  code: insufficient_scope
                  message: This PMS credential lacks the properties:read scope.
                  request_id: 0f3b8f8e-5f8c-4b84-9a8e-5d6c2a1e7b40
    RateLimited:
      description: Too many requests in the current minute. Wait for `Retry-After` seconds.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/RateLimitedError"
          examples:
            rateLimited:
              summary: More than 60 requests in a minute
              value:
                error:
                  code: rate_limited
                  message: Too many requests. Retry after the number of seconds in Retry-After.
                  request_id: c2b7e1d4-8a3f-4e6b-9d2c-7f1a5e9b3c08
  schemas:
    Health:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - ok
    Me:
      type: object
      additionalProperties: false
      required:
        - credential_id
        - pms_name
        - environment
        - scopes
      properties:
        credential_id:
          type: string
          format: uuid
          description: The public ID of the PMS credential that made the call. Not a secret.
        pms_name:
          type: string
          description: Your PMS, as Tatallo knows it.
        environment:
          type: string
          enum:
            - staging
            - production
            - development
          description: The environment the key belongs to. `development` is Tatallo-internal.
        scopes:
          type: array
          items:
            type: string
            enum:
              - properties:read
          description: What the key may do. `properties:read` lists and reads properties.
    Property:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - timezone
        - status
        - country
        - city
        - max_guests
        - currency
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: |
            The property's permanent, opaque identifier. Store it once in your mapping table: it
            never changes meaning. It is an identifier, not a credential: knowing it grants
            nothing.
        name:
          type: string
          description: The property's name on Tatallo.
        timezone:
          type:
            - string
            - "null"
          description: The IANA time zone of the property (for example `Europe/Rome`). Interpret stay dates and check-in times in it. May be `null` only while the property is `pending`.
          example: Europe/Rome
        status:
          type: string
          enum:
            - active
            - inactive
            - pending
          description: |
            Whether the property can sell: `active` can; `inactive` was suspended and cannot;
            `pending` is not live yet.
        country:
          type: string
          description: The property's country, by name.
        city:
          type: string
        max_guests:
          type: integer
          minimum: 1
          description: The most guests the property sleeps.
        currency:
          type: string
          pattern: ^[A-Z]{3}$
          description: The property's currency, ISO 4217.
        updated_at:
          type: string
          format: date-time
          description: When the property last changed, ISO 8601 in UTC.
    PageInfo:
      type: object
      additionalProperties: false
      required:
        - next_cursor
      properties:
        next_cursor:
          type:
            - string
            - "null"
          description: Pass it as `cursor` to get the next page. `null` on the last page.
    PropertyList:
      type: object
      additionalProperties: false
      required:
        - data
        - page
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Property"
        page:
          $ref: "#/components/schemas/PageInfo"
    Error:
      type: object
      additionalProperties: false
      required:
        - error
      description: |
        The one error shape. Branch on `code`: `unauthorized` (401), `insufficient_scope` (403),
        `property_not_found` (404), `invalid_cursor` (400), `invalid_limit` (400),
        `rate_limited` (429). `message` is English and may change; codes never do.
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              description: A stable, snake_case error code.
            message:
              type: string
              description: An English explanation for people. May change; never branch on it.
            request_id:
              type: string
              format: uuid
              description: The same value as the `X-Request-Id` header.
    UnauthorizedError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            error:
              type: object
              properties:
                code:
                  const: unauthorized
    InsufficientScopeError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            error:
              type: object
              properties:
                code:
                  const: insufficient_scope
    PropertyNotFoundError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            error:
              type: object
              properties:
                code:
                  const: property_not_found
    InvalidListQueryError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            error:
              type: object
              properties:
                code:
                  enum:
                    - invalid_limit
                    - invalid_cursor
    RateLimitedError:
      allOf:
        - $ref: "#/components/schemas/Error"
        - type: object
          properties:
            error:
              type: object
              properties:
                code:
                  const: rate_limited
