{
  "openapi": "3.1.0",
  "info": {
    "title": "Noovi POS Partner API",
    "version": "1.0.0-draft",
    "description": "Proposed contract for a POS partner calling Noovi. Implementation is pending. The example URLs are placeholders. Noovi provides the HTTPS base URL, company ID, and OAuth client registration for each partner connection and environment during onboarding."
  },
  "servers": [
    {
      "url": "https://noovi.example.test/api/pos/v1",
      "description": "Illustrative URL"
    }
  ],
  "security": [
    {
      "NooviOAuth": [],
      "MutualTLS": []
    }
  ],
  "tags": [
    {
      "name": "Tables",
      "description": "Read Noovi's physical table inventory and write POS mappings and observations."
    },
    {
      "name": "Reservations",
      "description": "Read Noovi booking and host state for the company's physical tables."
    },
    {
      "name": "Guests",
      "description": "Optional company- and connection-scoped guest import and export."
    }
  ],
  "paths": {
    "/companies/{companyId}/tables": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        }
      ],
      "get": {
        "tags": [
          "Tables"
        ],
        "operationId": "listTables",
        "summary": "List physical tables",
        "description": "List Noovi reservation resources of type table, including unmapped resources. A floor-plan element is eligible only when it has a physical reservation resource. Read POS observations and live Noovi host occupancy independently. A table without a currently seated party can still have a future reservation.",
        "responses": {
          "200": {
            "description": "Company table inventory.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/TableList"
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "tables": [
                      {
                        "posTableId": "table-12",
                        "resourceId": "R-RSC-DEMO12",
                        "name": "Table 12",
                        "capacity": 4,
                        "posStatus": {
                          "state": "occupied",
                          "observedAt": "2026-11-09T23:30:00Z"
                        },
                        "hostOccupancy": "seated"
                      }
                    ]
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "NooviOAuth": [
              "tables:read"
            ],
            "MutualTLS": []
          }
        ]
      }
    },
    "/companies/{companyId}/tables/{posTableId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        },
        {
          "$ref": "#/components/parameters/PosTableId"
        }
      ],
      "put": {
        "tags": [
          "Tables"
        ],
        "operationId": "upsertTable",
        "summary": "Create or update a POS table mapping",
        "description": "For a new POS table, supply resourceId to link an existing Noovi physical table, or omit it to create a new resource and link it. Check the inventory first to avoid creating duplicate physical tables. Repeating a PUT for the same company and POS table ID preserves the Noovi resource ID. An existing mapping cannot be reassigned with this endpoint. This operation changes table metadata and preserves existing bookings and POS status.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TableUpsert"
              },
              "example": {
                "name": "Table 12",
                "capacity": 4
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing mapping updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Table"
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "posTableId": "table-12",
                    "resourceId": "R-RSC-DEMO12",
                    "name": "Table 12",
                    "capacity": 4,
                    "posStatus": {
                      "state": "occupied",
                      "observedAt": "2026-11-09T23:30:00Z"
                    },
                    "hostOccupancy": "seated"
                  },
                  "error": null
                }
              }
            }
          },
          "201": {
            "description": "Mapping created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Table"
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "posTableId": "table-12",
                    "resourceId": "R-RSC-DEMO12",
                    "name": "Table 12",
                    "capacity": 4,
                    "posStatus": null,
                    "hostOccupancy": "free"
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "409": {
            "$ref": "#/components/responses/MappingConflict"
          }
        },
        "security": [
          {
            "NooviOAuth": [
              "tables:write"
            ],
            "MutualTLS": []
          }
        ]
      }
    },
    "/companies/{companyId}/tables/{posTableId}/status": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        },
        {
          "$ref": "#/components/parameters/PosTableId"
        }
      ],
      "put": {
        "tags": [
          "Tables"
        ],
        "operationId": "putTableStatus",
        "summary": "Publish a POS table observation",
        "description": "Register the table mapping before publishing status. Noovi stores the observation separately from reservation and host state. Compare timestamps as UTC instants. An older observation, or a different state at the same observedAt, returns 409. An exact replay succeeds without another change.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PosTableStatus"
              },
              "example": {
                "state": "occupied",
                "observedAt": "2026-11-09T23:30:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Observation applied or already current.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/TableStatusResult"
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "posTableId": "table-12",
                    "posStatus": {
                      "state": "occupied",
                      "observedAt": "2026-11-09T23:30:00Z"
                    }
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "409": {
            "$ref": "#/components/responses/ObservationConflict"
          }
        },
        "security": [
          {
            "NooviOAuth": [
              "tables:write"
            ],
            "MutualTLS": []
          }
        ]
      }
    },
    "/companies/{companyId}/reservations": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        }
      ],
      "get": {
        "tags": [
          "Reservations"
        ],
        "operationId": "listReservations",
        "summary": "Read Noovi reservation status",
        "description": "Read bookings overlapping the half-open UTC window [from, to), using startTime < to and endTime > from. The result includes booking status and host party state without guest identity, contact details or notes. A reservation maps to a POS table only when its Noovi table is assigned and the resource has a POS mapping.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "Window start, inclusive.",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/UtcTimestamp",
              "description": "Use a UTC timestamp.",
              "examples": [
                "2026-11-09T23:00:00Z"
              ]
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Window end, exclusive and strictly after from.",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/UtcTimestamp",
              "description": "Use a UTC timestamp.",
              "examples": [
                "2026-11-10T02:00:00Z"
              ]
            }
          },
          {
            "name": "posTableId",
            "in": "query",
            "description": "Limit results to this mapped POS table. An unknown mapping returns 404.",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Identifier"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bookings in the requested company and time window.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/ReservationList"
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "reservations": [
                      {
                        "reservationId": "res_demo",
                        "posTableId": "table-12",
                        "startTime": "2026-11-09T23:00:00Z",
                        "endTime": "2026-11-10T00:30:00Z",
                        "state": "confirmed",
                        "partyState": "seated",
                        "partySize": 4
                      }
                    ]
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "NooviOAuth": [
              "reservations:read"
            ],
            "MutualTLS": []
          }
        ]
      }
    },
    "/companies/{companyId}/guests": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        }
      ],
      "get": {
        "tags": [
          "Guests"
        ],
        "operationId": "listExportableGuests",
        "summary": "Export eligible Noovi guest profiles",
        "description": "Requires guests:read and guest export enabled for this company and connection. Returns only profiles explicitly eligible for this recipient, with purpose-limited names and contact fields. Profiles received by import alone are excluded until explicitly approved for export. Start without a cursor and follow nextCursor until null to read the current export. The export is paginated, and updatedAt describes each profile. Arrange data removal through the agreed operational process.",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque nextCursor from the previous response for this company and connection. Omit on the first request.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "demo-next-page"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum profiles to return per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            },
            "example": 100
          }
        ],
        "responses": {
          "200": {
            "description": "One page of profiles currently eligible for this recipient.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/GuestPage"
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "guests": [
                      {
                        "guestId": "CUST-DEMO42",
                        "posGuestId": null,
                        "firstName": "Morgan",
                        "lastName": "Lee",
                        "phoneNumber": "+12025550173",
                        "emailAddress": "morgan@example.test",
                        "updatedAt": "2026-11-09T23:15:00Z"
                      }
                    ],
                    "nextCursor": null
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/GuestSharingForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "NooviOAuth": [
              "guests:read"
            ],
            "MutualTLS": []
          }
        ]
      }
    },
    "/companies/{companyId}/guests/{posGuestId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CompanyId"
        },
        {
          "$ref": "#/components/parameters/PosGuestId"
        }
      ],
      "put": {
        "tags": [
          "Guests"
        ],
        "operationId": "upsertPosGuest",
        "summary": "Import a POS guest profile",
        "description": "Requires guests:write and guest import enabled for this company and connection. Upsert using the stable company, authenticated connection and posGuestId combination. Repeat submissions for that key update the linked profile without duplicating it. Omitted optional fields preserve previously imported values. An explicit null clears the partner-supplied value. If phone or email matches an existing unlinked profile, or conflicts with another profile, return 409 and resolve the link separately. Contact fields are for the agreed service purpose. Start newly imported profiles with marketing channels disabled and collect marketing consent separately.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GuestUpsert"
              },
              "example": {
                "firstName": "Morgan",
                "lastName": "Lee",
                "phoneNumber": "+12025550173",
                "emailAddress": "morgan@example.test"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing linked profile updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/GuestResult"
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "guest": {
                      "guestId": "CUST-DEMO42",
                      "posGuestId": "guest-42",
                      "firstName": "Morgan",
                      "lastName": "Lee",
                      "phoneNumber": "+12025550173",
                      "emailAddress": "morgan@example.test",
                      "updatedAt": "2026-11-09T23:15:00Z"
                    }
                  },
                  "error": null
                }
              }
            }
          },
          "201": {
            "description": "Profile and partner link created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "success",
                    "data",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/GuestResult"
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "guest": {
                      "guestId": "CUST-DEMO42",
                      "posGuestId": "guest-42",
                      "firstName": "Morgan",
                      "lastName": "Lee",
                      "phoneNumber": "+12025550173",
                      "emailAddress": "morgan@example.test",
                      "updatedAt": "2026-11-09T23:15:00Z"
                    }
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/GuestSharingForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "409": {
            "$ref": "#/components/responses/GuestMatchConflict"
          }
        },
        "security": [
          {
            "NooviOAuth": [
              "guests:write"
            ],
            "MutualTLS": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "NooviOAuth": {
        "type": "oauth2",
        "description": "Use OAuth 2.0 client credentials with the Noovi-operated issuer. Send client_id, grant_type=client_credentials, and the requested scope to the token endpoint using the registered client certificate. The issuer restricts scopes, audience, and company to that client registration and issues a short-lived [RFC 9068](https://www.rfc-editor.org/rfc/rfc9068.html) JWT access token bound to the certificate. Present Authorization: Bearer <access_token> and the same client certificate on API requests. The receiver validates the token type, signature using trusted issuer keys and configured algorithms, issuer (example https://auth.noovi.example.test), audience (urn:noovi:pos:api), expiry, certificate confirmation (cnf.x5t#S256), operation scope, client registration, and company_id against the request path. Use separate registrations and keys per company connection, integration direction, and environment.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://auth.noovi.example.test/oauth/token",
            "scopes": {
              "tables:read": "Read the company's physical table inventory.",
              "tables:write": "Upsert POS table mappings and report physical table observations.",
              "reservations:read": "Read the company's minimal reservation and party state.",
              "guests:read": "Read profiles eligible for export to the connected partner.",
              "guests:write": "Import profiles from the connected partner."
            }
          }
        }
      },
      "MutualTLS": {
        "type": "mutualTLS",
        "description": "Present the registered client certificate on the token request and the same certificate on resource requests. The receiver checks the SHA-256 thumbprint from the verified TLS connection against the token's cnf.x5t#S256 claim under RFC 8705. Keep the private key on the calling server and coordinate certificate rotation with Noovi."
      }
    },
    "parameters": {
      "CompanyId": {
        "name": "companyId",
        "in": "path",
        "required": true,
        "description": "Noovi company ID issued at onboarding. It must match the access token's company_id claim and the authenticated client's company registration.",
        "schema": {
          "$ref": "#/components/schemas/Identifier"
        },
        "example": "cmp_demo"
      },
      "PosTableId": {
        "name": "posTableId",
        "in": "path",
        "required": true,
        "description": "Stable POS table ID within this company. URL-encode the path segment.",
        "schema": {
          "$ref": "#/components/schemas/Identifier"
        },
        "example": "table-12"
      },
      "PosGuestId": {
        "name": "posGuestId",
        "in": "path",
        "required": true,
        "description": "Stable guest identifier within the partner connection and company. URL-encode this path segment.",
        "schema": {
          "$ref": "#/components/schemas/Identifier"
        },
        "example": "guest-42"
      }
    },
    "schemas": {
      "Identifier": {
        "type": "string",
        "minLength": 1,
        "maxLength": 128
      },
      "UtcTimestamp": {
        "type": "string",
        "format": "date-time",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?Z$",
        "description": "ISO 8601 UTC with a Z suffix and optional fractional seconds."
      },
      "TableUpsert": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "capacity"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Display name from the POS.",
            "examples": [
              "Table 12"
            ]
          },
          "capacity": {
            "type": "integer",
            "minimum": 1,
            "description": "Number of seats."
          },
          "resourceId": {
            "$ref": "#/components/schemas/Identifier",
            "description": "Optional existing Noovi physical table to link on first PUT. Later PUTs may omit it or send the same ID.",
            "examples": [
              "R-RSC-DEMO12"
            ]
          }
        }
      },
      "PosTableStatus": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "state",
          "observedAt"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "available",
              "occupied",
              "unavailable"
            ],
            "description": "Partner's current observation of the physical table.",
            "examples": [
              "occupied"
            ]
          },
          "observedAt": {
            "$ref": "#/components/schemas/UtcTimestamp",
            "description": "Time this state was observed at the POS.",
            "examples": [
              "2026-11-09T23:30:00Z"
            ]
          }
        }
      },
      "Table": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "posTableId",
          "resourceId",
          "name",
          "capacity",
          "posStatus",
          "hostOccupancy"
        ],
        "properties": {
          "posTableId": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Identifier"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null before a POS mapping exists."
          },
          "resourceId": {
            "$ref": "#/components/schemas/Identifier",
            "description": "Stable Noovi reservation resource ID."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "capacity": {
            "type": "integer",
            "minimum": 1
          },
          "posStatus": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PosTableStatus"
              },
              {
                "type": "null"
              }
            ],
            "description": "Most recent POS observation, or null before the first update."
          },
          "hostOccupancy": {
            "type": "string",
            "enum": [
              "free",
              "seated",
              "needs_cleaning",
              "unknown"
            ],
            "description": "Noovi host state. An uncleared departure is needs_cleaning. Unknown is used when occupancy cannot be determined.",
            "examples": [
              "seated"
            ]
          }
        }
      },
      "TableList": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "tables"
        ],
        "properties": {
          "tables": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Table"
            }
          }
        }
      },
      "TableStatusResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "posTableId",
          "posStatus"
        ],
        "properties": {
          "posTableId": {
            "$ref": "#/components/schemas/Identifier"
          },
          "posStatus": {
            "$ref": "#/components/schemas/PosTableStatus"
          }
        }
      },
      "Reservation": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "reservationId",
          "posTableId",
          "startTime",
          "endTime",
          "state",
          "partyState",
          "partySize"
        ],
        "properties": {
          "reservationId": {
            "$ref": "#/components/schemas/Identifier"
          },
          "posTableId": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Identifier"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null for unassigned or unmapped reservations. A capacity hold is not a table assignment."
          },
          "startTime": {
            "$ref": "#/components/schemas/UtcTimestamp",
            "description": "Scheduled start of the booking.",
            "examples": [
              "2026-11-09T23:00:00Z"
            ]
          },
          "endTime": {
            "$ref": "#/components/schemas/UtcTimestamp",
            "description": "Scheduled end of the booking.",
            "examples": [
              "2026-11-10T00:30:00Z"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "initiated",
              "pending",
              "confirmed",
              "completed",
              "no_show",
              "canceled"
            ],
            "description": "Noovi reservation lifecycle.",
            "examples": [
              "confirmed"
            ]
          },
          "partyState": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "expected",
                  "arrived",
                  "seated",
                  "left"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Noovi host party state, or null when unrecorded."
          },
          "partySize": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1
              },
              {
                "type": "null"
              }
            ],
            "description": "Party size, when recorded."
          }
        }
      },
      "ReservationList": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "reservations"
        ],
        "properties": {
          "reservations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Reservation"
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "success",
          "data",
          "error"
        ],
        "properties": {
          "success": {
            "const": false
          },
          "data": {
            "type": "null"
          },
          "error": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "GuestUpsert": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "phoneNumber"
        ],
        "properties": {
          "firstName": {
            "description": "Partner-supplied first name.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              {
                "type": "null"
              }
            ]
          },
          "lastName": {
            "description": "Partner-supplied last name.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              {
                "type": "null"
              }
            ]
          },
          "phoneNumber": {
            "type": "string",
            "pattern": "^\\+[1-9][0-9]{1,14}$",
            "description": "Phone number in international E.164 form."
          },
          "emailAddress": {
            "description": "Partner-supplied email address.",
            "anyOf": [
              {
                "type": "string",
                "format": "email",
                "maxLength": 254
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "Guest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "guestId",
          "posGuestId",
          "firstName",
          "lastName",
          "phoneNumber",
          "emailAddress",
          "updatedAt"
        ],
        "properties": {
          "guestId": {
            "$ref": "#/components/schemas/Identifier",
            "description": "Stable Noovi guest ID."
          },
          "posGuestId": {
            "description": "Guest ID in the requesting POS connection, when linked.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/Identifier"
              },
              {
                "type": "null"
              }
            ]
          },
          "firstName": {
            "description": "First name, when available and shared.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              {
                "type": "null"
              }
            ]
          },
          "lastName": {
            "description": "Last name, when available and shared.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              {
                "type": "null"
              }
            ]
          },
          "phoneNumber": {
            "description": "Phone number, when available and shared.",
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\+[1-9][0-9]{1,14}$",
                "description": "Phone number in international E.164 form."
              },
              {
                "type": "null"
              }
            ]
          },
          "emailAddress": {
            "description": "Email address, when available and shared.",
            "anyOf": [
              {
                "type": "string",
                "format": "email",
                "maxLength": 254
              },
              {
                "type": "null"
              }
            ]
          },
          "updatedAt": {
            "$ref": "#/components/schemas/UtcTimestamp",
            "description": "Time the guest profile was last updated."
          }
        }
      },
      "GuestResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "guest"
        ],
        "properties": {
          "guest": {
            "$ref": "#/components/schemas/Guest"
          }
        }
      },
      "GuestPage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "guests",
          "nextCursor"
        ],
        "properties": {
          "guests": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Guest"
            }
          },
          "nextCursor": {
            "description": "Opaque token for the next page, or null at the end of the export.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ]
          }
        }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "Invalid parameters or JSON body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "INVALID_REQUEST",
                "message": "Check the request fields."
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Access token missing or invalid, or token and client certificate binding mismatch.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "UNAUTHORIZED",
                "message": "A valid certificate-bound Noovi access token is required."
              }
            }
          }
        },
        "headers": {
          "WWW-Authenticate": {
            "description": "OAuth bearer challenge for a request that reached the HTTP layer.",
            "schema": {
              "type": "string"
            },
            "example": "Bearer"
          }
        }
      },
      "Forbidden": {
        "description": "Authenticated client is outside the company binding or requested operation scope.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "FORBIDDEN",
                "message": "The access token does not authorize this company or operation."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Company, mapped table, or supplied resource could not be found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "NOT_FOUND",
                "message": "The requested resource was not found."
              }
            }
          }
        }
      },
      "MappingConflict": {
        "description": "The requested table mapping conflicts with a mapping already in use.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "TABLE_MAPPING_CONFLICT",
                "message": "The table is already mapped."
              }
            }
          }
        }
      },
      "ObservationConflict": {
        "description": "A newer or conflicting POS observation is already recorded.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "TABLE_STATUS_CONFLICT",
                "message": "The observation cannot replace the recorded status."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "RATE_LIMITED",
                "message": "Retry after the indicated delay."
              }
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected processing failure.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "INTERNAL_ERROR",
                "message": "The operation could not be completed."
              }
            }
          }
        }
      },
      "GuestSharingForbidden": {
        "description": "Company or connection scope mismatch, permission missing, or guest sharing disabled.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "GUEST_SHARING_FORBIDDEN",
                "message": "Guest sharing is unavailable for this request."
              }
            }
          }
        }
      },
      "GuestMatchConflict": {
        "description": "Existing or conflicting guest profile requires explicit resolution.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "success": false,
              "data": null,
              "error": {
                "code": "GUEST_MATCH_CONFLICT",
                "message": "The guest profile could not be linked automatically."
              }
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "RFC 8705 mutual TLS client authentication and certificate-bound access tokens",
    "url": "https://www.rfc-editor.org/rfc/rfc8705.html"
  }
}
