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: developers@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:
                  $ref: './examples/health-ok.json'
        '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:
                  $ref: './examples/me.json'
        '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:
                  $ref: './examples/property-list.json'
                firstOfSeveralPages:
                  $ref: './examples/property-list-first-page.json'
        '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:
                  $ref: './examples/error-invalid-limit.json'
                invalidCursor:
                  $ref: './examples/error-invalid-cursor.json'
        '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:
                  $ref: './examples/property-active.json'
                inactive:
                  $ref: './examples/property-inactive.json'
        '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:
                  $ref: './examples/error-property-not-found.json'
        '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:
              $ref: './examples/error-unauthorized.json'
    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:
              $ref: './examples/error-insufficient-scope.json'
    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:
              $ref: './examples/error-rate-limited.json'
  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
