{
  "$schema": "./_mapping.schema.json",
  "provider": "opentable",
  "name": "OpenTable",
  "vertical": "restaurants",
  "verified_on": "2026-10-25",
  "docs": [
    {
      "title": "OpenTable API Documentation (Postman documenter)",
      "url": "https://docs.opentable.com/",
      "note": "Public, no login. Anchors below (#<uuid>) are the Postman item ids of each section in the published collection. partner-connect/opentable/fixtures/collection-examples.json holds every example body the client is tested against."
    },
    {
      "title": "Same collection as JSON (what this file was read from)",
      "url": "https://docs.opentable.com/api/collections/38767429/2sAXxTcWRy?segregateAuth=true&versionTag=latest"
    },
    { "title": "Authorization: Obtaining an Access Token", "url": "https://docs.opentable.com/#ce3a8f78-70b7-480f-8902-51219c6d0ae5" },
    { "title": "Online Booking API v2: GET Single RID Search V2 (availability)", "url": "https://docs.opentable.com/#96764532-9012-4fa4-b2fc-4b1fb9345085" },
    { "title": "Online Booking API v2: Availability Metadata (dining areas)", "url": "https://docs.opentable.com/#cc623732-2316-454f-873e-0d93e4ea9833" },
    { "title": "Online Booking API v2: Get Active Experiences V2", "url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
    { "title": "Online Booking API v2: Cancellation Policies (deposit details)", "url": "https://docs.opentable.com/#a2b91ea7-e2b8-4de1-b677-fe25f144a15c" },
    { "title": "Online Booking API v2: Get Reservation", "url": "https://docs.opentable.com/#82bcee3c-bcb6-48ca-90e8-b6dbe1688720" },
    { "title": "Sync APIs: Copper Reservation Webhook (v3, Feb 2025)", "url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0", "note": "Same webhook is documented under Gold, Platinum and POS API (Reservation Webhook v3 Copper)." },
    { "title": "Online Booking API v2: Experience Total Calculation", "url": "https://docs.opentable.com/#8674d747-1e77-4c21-9112-7bc478716c90" },
    { "title": "Online Booking API v2: Slot Locks (create)", "url": "https://docs.opentable.com/#24d0dfcc-80c6-4cf8-9e8d-65f29635d4a2" },
    { "title": "Online Booking API v2: Slot Locks (delete)", "url": "https://docs.opentable.com/#03bfbdef-9fee-47a9-87b3-ffa235345d05" },
    { "title": "Online Booking API v2: Make Reservation (Spreedly card fields, SCA, common errors)", "url": "https://docs.opentable.com/#b1c1ac75-7821-4263-b3d3-e1c5c2ff4fca" },
    { "title": "Online Booking API v2: Booking Policies", "url": "https://docs.opentable.com/#0290fb98-be7b-4789-8458-00bd2b8c8d0c" },
    { "title": "Online Booking API v2: Modify Reservation", "url": "https://docs.opentable.com/#0fc6793e-5f32-4b5e-a9d8-c598d53c326b" },
    { "title": "Online Booking API v2: Cancel Web Reservation", "url": "https://docs.opentable.com/#454b5367-6a17-46d6-ae4c-456f55ae688b" },
    { "title": "Online Booking API v2: Payment Profile (Spreedly tokenizer)", "url": "https://docs.opentable.com/#bcd44bf5-b59b-4aa5-971d-1021b600f156" },
    { "title": "Online Booking API v2: Private Dining Lead", "url": "https://docs.opentable.com/#990fadac-b956-4328-959a-bad00a9c128a" },
    { "title": "Online Booking API v2: Partner Notes (create / update)", "url": "https://docs.opentable.com/#829c10c8-2d72-445b-b1e5-b3f82471d3bc" },
    { "title": "Online Booking API v2: Partner Reservation Confirmation", "url": "https://docs.opentable.com/#52ac7295-823b-41e7-889e-e860a415d3bc" },
    { "title": "Online Booking API v2: Tips for Successful Implementation (test rids, certification)", "url": "https://docs.opentable.com/#5b64f591-de88-427f-b950-001fbc63e487" },
    { "title": "Directory API (consumer partners only)", "url": "https://docs.opentable.com/#b9b311ee-1284-4fa4-861f-e20ae5417b5f" },
    { "title": "Change Log: Spreedly as a payment processor", "url": "https://docs.opentable.com/#cbe244b4-98d8-43f3-9d1c-67c812abe135" },
    { "title": "Change Log: Service Fees for Booking APIs (deadline 2026-09-01)", "url": "https://docs.opentable.com/#af949cfc-c0dc-466f-804d-c045bd51da2c" }
  ],
  "access": {
    "public_docs": true,
    "how_to_get_access": "Docs are public; credentials are not. OpenTable approves partners via the application form linked from the docs' introduction ('For partnership inquiries, please fill out our application form'). Sync API data (and its reservation webhook) comes in Copper / Gold / Platinum tiers; the venue enables the partner through the Integrations Marketplace tile (OAuth, unique identifier or delayed-email onboarding).",
    "auth": "OAuth 2.0 client credentials: GET {oauth-url}/api/v2/oauth/token?grant_type=client_credentials with Authorization: Basic <client_id:secret>; returns a Bearer access_token with expires_in (seconds; 1989434 in the example). No refresh grant: request a new one. partner-connect/opentable/client.ts caches it per instance and renews it 5 minutes early.",
    "base_url": "API host not public: the docs use {{base-url}}; examples show the QA host https://platform.otqa.com. The Authorization doc names the OAuth hosts: pre-production https://oauth-pp.opentable.com, production https://oauth.opentable.com. Onboarding gives pre-production test restaurants only; production credentials follow OpenTable's QA certification (at least 14 days)."
  },
  "splitclub": {
    "integration_provider": "opentable",
    "recommended_path": "webhook",
    "note": "Built 2026-10-25, off until OpenTable issues credentials and PARTNER_CONNECT_ENABLED lists opentable. Catalog: providers/opentable.ts maps active experiences to Offerings (read-only). Booking: partner-connect/opentable/client.ts covers the Online Booking API v2 (availability and metadata, experiences and their total, booking and cancellation policies, payment profile, slot lock and release, make / get / modify / cancel, partner notes and confirmation, private dining leads) and the Directory API; book() locks, books and releases the lock on any failure. Reservations that need a card (deposit, hold, prepaid experience: the slot lock answers payment_method, or the booking answers CreditCardRequired or SCA) are refused and reported as card_required: card data goes through Spreedly and SplitClub never handles it. Ownership: SplitClub's credentials are partner-wide, so a venue's typed rid proves nothing; the connection waits in needs_review until staff confirm the restaurant (Integrations Marketplace enablement or the restaurant directly) with ops_opentable_connection_approve (migration 20261025390000), one connection per rid. Webhook: POST https://splitclubapp.com/api/v1/connections/hooks/opentable. Each approved connection mints its own secret (POST /v1/connections/:id/webhook) that OpenTable sends as 'Authorization: Bearer <secret>' (Basic with it as the password works too); the receiver compares hashes in constant time against the connections for the delivery's rid and keeps the newest state per reservation in public.opentable_reservations (migration 20261025400000). The endpoint is https on 443, doesn't redirect and resolves to a public IP. The older generic path (POST /v1/integrations, provider opentable, staff-reviewed) still exists."
  },
  "objects": {
    "Offering": {
      "source": "GET {base-url}/v2/experiences/{rid}/active → data[]",
      "kind": "dining",
      "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93",
      "note": "One Offering per experience. Plain tables are better modelled from GET /v2/availability-metadata/{rid} → data.dining_areas[] (id, name, description, environment) as kind table, with the same field pattern. Pointers are rooted at the SplitClub Offering.",
      "fields": [
        { "to": "/external_id", "from": "data[].experience_id", "transform": "number → string, prefix ot_exp_", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/title", "from": "data[].name", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/description", "from": "data[].description", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/kind", "from": "data[].type_enum", "transform": "PRIX_FIXE and other menu types → dining. Only PRIX_FIXE is shown in the docs; event-style type_enum values are not listed, so ticketed_event cannot be detected yet.", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/media/0/url", "from": "data[].experience_images[]", "transform": "the image object's shape is not documented ('a list of image assets')", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93", "unverified": true },
        { "to": "/metadata/opentable_rid", "from": "data[].rid", "transform": "number → string", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/booking_url", "from": "times_available[].availability_types[].dining_area[].booking_url (GET /v2/availability)", "transform": "per slot; use the restaurant-level restref URL without dateTime as a default", "source_url": "https://docs.opentable.com/#96764532-9012-4fa4-b2fc-4b1fb9345085" },
        { "to": "/policy/cancellation/0/hours_before", "from": "cutOff.daysBeforeCutoff (GET /v2/cancellation-policies/{rid}/{id})", "transform": "days × 24 when cutOff.cutoffType = DaysBefore; refund_pct 100 before the cutoff, 0 after (refund % is not stated)", "source_url": "https://docs.opentable.com/#a2b91ea7-e2b8-4de1-b677-fe25f144a15c" }
      ]
    },
    "PriceTier": {
      "source": "GET {base-url}/v2/experiences/{rid}/active → data[].price_info.prices[]; deposits from GET /v2/cancellation-policies/{rid}/{id}",
      "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93",
      "note": "OpenTable money is an integer plus a multiplier/denominator (100 for USD cents). The Individual Price Option object has no field table in the docs; its fields come from the documented sample response.",
      "fields": [
        { "to": "/tiers/0/external_id", "from": "price_info.prices[].priceId", "transform": "number → string", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/tiers/0/label", "from": "price_info.prices[].priceTitle", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/tiers/0/amount", "from": "price_info.prices[].minUnitAmount", "transform": "integer at price_info.multiplier → minor units: round(minUnitAmount × 10^exponent(currency) / multiplier); identity when multiplier = 100 and currency is USD", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/tiers/0/currency", "from": "price_info.currencyCode", "transform": "upper → lower case", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/tiers/0/unit", "from": "price_info.priceType", "transform": "PER_PERSON → per_person; PER_PARTY → per_group", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/tiers/0/fees/0/percent", "from": "price_info.gratuity.numerator / price_info.gratuity.denominator", "transform": "× 100 (e.g. 350/10000 → 3.5); type gratuity; included = false (priceAllInclusive true → included = true); only when gratuity.mandatory", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/tiers/0/fees/0/included", "from": "price_info.prices[].priceAllInclusive", "source_url": "https://docs.opentable.com/#790cbb98-1599-4fe6-b427-defe5a697d93" },
        { "to": "/tiers/0/deposit/amount", "from": "depositDetails.amount (cancellation policy) or times_available[].availability_types[].cancellation_policy.amount", "transform": "integer in minor units at depositDetails.denominator (100 = cents). depositDetails.type / deposit_type PerGuest → deposit is per person: if the tier unit is per_group multiply by party size.", "source_url": "https://docs.opentable.com/#a2b91ea7-e2b8-4de1-b677-fe25f144a15c" },
        { "to": "/tiers/0/minimum_spend", "from": "none", "transform": "OpenTable has no documented minimum-spend field", "unverified": true }
      ]
    },
    "Availability": {
      "source": "GET {base-url}/v2/availability/{rid}?start_date_time=…&forward_minutes=…&backward_minutes=…&party_size=…&include_experiences=true",
      "source_url": "https://docs.opentable.com/#96764532-9012-4fa4-b2fc-4b1fb9345085",
      "note": "Search, not a schedule: it answers for one party size and a time window. Poll per offering and party size and post the resulting slots. Pointers are rooted at the SplitClub Offering. A slot whose availability_types[] carries cancellation_policy (Deposit / Hold) or experience_cancellation_policies[] (Hold / Prepayment) needs a card to book; the client flags it (requires_card) and won't book it. The dining-area list is documented as dining_area but some examples spell it diningArea; both are read.",
      "fields": [
        { "to": "/availability/slots/0/starts_at", "from": "times_available[].time", "transform": "local wall-clock 'YYYY-MM-DDTHH:mm' with no offset → attach the restaurant's offset. No documented response here carries the restaurant timezone, so it must come from SplitClub's venue record.", "source_url": "https://docs.opentable.com/#96764532-9012-4fa4-b2fc-4b1fb9345085" },
        { "to": "/availability/slots/0/external_id", "from": "rid + times_available[].time + dining_area[].id", "transform": "concatenate; OpenTable gives slots no id", "source_url": "https://docs.opentable.com/#96764532-9012-4fa4-b2fc-4b1fb9345085" },
        { "to": "/availability/slots/0/capacity", "from": "const:1", "transform": "OpenTable returns a time only if a table for party_size is free; it never returns a count", "source_url": "https://docs.opentable.com/#96764532-9012-4fa4-b2fc-4b1fb9345085" },
        { "to": "/availability/timezone", "from": "none", "transform": "not in the documented availability, metadata or experiences responses; use the venue's IANA zone held by SplitClub", "unverified": true }
      ]
    },
    "Checkout": {
      "source": "Sync API Reservation Webhook v3 payload (or GET /sync/v2/reservations, GET /v2/booking/{rid}/reservations/{rid}-{confirmation_id})",
      "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0",
      "fields": [
        { "to": "/external_booking_id", "from": "id", "transform": "string, 'a globally unique identifier for the reservation'", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
        { "to": "/party_size", "from": "party_size", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
        { "to": "/amount", "from": "experience_details.total_amount", "transform": "integer in minor units (sample: 26200 = 200.00 subtotal + 55.00 add-ons + fees, tax, tip). The webhook's own denominator is only shown on add_ons[].CurrencyDenominator (0 in the sample) and deposit_details.currencyDenominator (100); confirm with GET Reservation's experience.currencyDenominator. Absent when no experience → venue must supply the amount.", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0", "unverified": true },
        { "to": "/currency", "from": "deposit_details.currency, or experience.currency from GET Reservation", "transform": "upper → lower case; deposit_details.currency is null when there is no deposit, so fall back to Get Reservation", "source_url": "https://docs.opentable.com/#82bcee3c-bcb6-48ca-90e8-b6dbe1688720" },
        { "to": "/offering_external_id", "from": "experience_details.experience_id", "transform": "same prefix as Offering.external_id", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
        { "to": "/description", "from": "experience_details.experience_title + scheduled_time", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
        { "to": "/pay_by", "from": "scheduled_time_utc", "transform": "documented as UTC but the sample has no 'Z' (2024-12-19T23:00:00) → parse as UTC, render with the venue's offset, minus the venue's cutoff. scheduled_time is local time without offset.", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
        { "to": "/metadata/confirmation_id", "from": "confirmation_id", "transform": "integer → string", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
        { "to": "/metadata/opentable_rid", "from": "rid", "transform": "integer → string", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
        { "to": "/metadata/deposit_paid", "from": "deposit_details.totalPaid", "transform": "integer at deposit_details.currencyDenominator → string of minor units; subtract from amount if the deposit was already taken", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
        { "to": "/return_url", "from": "const:<venue's registered origin>", "transform": "not from OpenTable; SplitClub config per venue" },
        { "to": "/status", "from": "status (GET Reservation)", "transform": "Pending, Seated, AssumedSeated leave the Checkout open; CancelledWeb, CancelledRestaurant, Cancelled, NoShow → canceled", "source_url": "https://docs.opentable.com/#82bcee3c-bcb6-48ca-90e8-b6dbe1688720" }
      ]
    }
  },
  "webhooks": {
    "supported": true,
    "signature": {
      "header": "Authorization",
      "algorithm": "none: 'Basic or bearer token is required to verify authenticity of request' — a static credential the partner configures. No HMAC. SplitClub gives each connection its own secret (sc_conn_…, only its sha256 stored) and compares it in constant time with those of the connections whose rid matches the delivery's rid.",
      "signed_payload": "nothing is signed",
      "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0"
    },
    "events": [
      {
        "event": "Reservation Webhook (v3)",
        "maps_to": "upsert public.opentable_reservations by (connection, id), forward only: a later updated_at_utc, or the same with a higher sequence_id (sequence_id isn't documented as a version, so it only breaks ties); state Cancelled or NoShow → ended, i.e. cancel the open Checkout. The verbatim delivery goes to partner_connection_events.",
        "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0",
        "fields": [
          { "to": "/external_booking_id", "from": "id", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
          { "to": "/status", "from": "state", "transform": "Cancelled, NoShow → canceled; all other states (NotConfirmed … AssumedDone) leave the Checkout as is", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" },
          { "to": "/party_size", "from": "party_size", "source_url": "https://docs.opentable.com/#0a4321d8-7055-4669-a186-345a8fd695d0" }
        ]
      }
    ],
    "note": "One unnamed event: a JSON POST 'on any change in reservation status' with the reservation attributes (no event-type field; branch on state). Endpoint must be https on port 443, no redirects, public IP. Retries use exponential backoff and stop after three failed attempts. The webhook carries fewer attributes than GET /sync/v2/reservations, and OpenTable may add attributes at any time, so ingestion must ignore unknown keys. Gold and Platinum add referrer, added_to_waitlist, cancellation_date, quoted_wait_time and sms_opt_in. Receiver: partner-connect POST /v1/connections/hooks/opentable."
  },
  "gaps": [
    "No webhook signature: only a static Basic/Bearer Authorization header, handled with a per-connection secret. The docs don't say whether OpenTable configures the webhook per partner or per restaurant; if only one partner-wide header is possible, every OpenTable connection would have to be given the same secret.",
    "Card bookings (deposits, card holds, prepaid experiences) need the diner's own device to tokenise the card with Spreedly plus a 3DS redirect flow. Not built: the client refuses them and reports card_required.",
    "The Service Fees changelog requires POST /v3/booking/{rid}/reservations/deposits/total and /holds/total before deposit and hold bookings, but documents only their responses, not their request bodies. Not implemented (those bookings are refused anyway).",
    "Booking Policies path: the date is YEAR-MONTH-DAY but the time is only shown as '7:00, 7:15'; HH:mm is sent, unverified.",
    "Make Reservation phone.country_code: the example sends 'US'; the scheme (ISO 3166 alpha-2 assumed) isn't stated.",
    "Two example request bodies contradict their field tables (restaurant_email_marketing_opt_in 'true' for a Boolean, dining_area_id '2632' for an Integer); the client sends the documented types.",
    "Slot lock expires_at has no offset; the Spreedly changelog calls it UTC, the Slot Lock doc only 'ISO 8601'.",
    "Partner Notes and Partner Confirmation are 'only applicable to specific partners', and the Directory API is 'only available to our Consumer Partners and is not offered to our Restaurant Partners'. Which SplitClub gets depends on its contract.",
    "Private Dining Lead answers the new lead's id as text/plain, with no response table.",
    "No restaurant timezone in the documented availability / experiences / webhook responses; times are local without offset (scheduled_time, times_available[].time) or UTC without 'Z' (scheduled_time_utc in the sample).",
    "Availability is a party-size search with no capacity count and no slot ids; ends_at / duration is not returned.",
    "No minimum-spend concept; table offerings come only from dining_areas (no prices attached).",
    "The webhook's money denominator for experience_details amounts is not stated (add-on CurrencyDenominator is 0 in the sample); verify against GET Reservation before trusting total_amount.",
    "The Individual Price Option object (priceId, priceTitle, minUnitAmount, priceAllInclusive) is shown only in a sample, not a field table. experience_images item shape is undocumented.",
    "Production base URLs, the partner application form and Sync tier pricing are not in the public docs; the webhook is partner/tier-gated.",
    "Cancellation policy gives a cutoff but not the refund percentage before/after it."
  ]
}
