{
  "$schema": "./_mapping.schema.json",
  "provider": "peek",
  "name": "Peek Pro (Peek Reseller API, OCTO)",
  "vertical": "tours_rentals",
  "verified_on": "2026-10-13",
  "docs": [
    {
      "title": "Peek Reseller API: Introduction",
      "url": "https://octodocs.peek.com/",
      "note": "Public. Peek Pro's implementation of the OCTO open standard, for resellers selling Peek Pro suppliers' products, with the octo/pricing and octo/webhooks capabilities. Glossary: Product = the activity or rental, Option (one per product for now), Unit = a ticket type, Unit Item = one line per unit in a booking. Every page is also served as Markdown by appending .md."
    },
    { "title": "Basics (auth)", "url": "https://octodocs.peek.com/getting-started/basics", "note": "Bearer token; HTTPS only; JSON bodies." },
    { "title": "Request Access", "url": "https://octodocs.peek.com/getting-started/request-access", "note": "Email Peek's product team for an OCTO API key." },
    { "title": "Capabilities", "url": "https://octodocs.peek.com/getting-started/capabilities", "note": "Peek enables all its capabilities on every request (octo/pricing, octo/webhooks); the Octo-Capabilities response header echoes them." },
    { "title": "Products", "url": "https://octodocs.peek.com/booking-flow/products" },
    { "title": "Availability", "url": "https://octodocs.peek.com/booking-flow/availability" },
    { "title": "Booking", "url": "https://octodocs.peek.com/booking-flow/booking", "note": "Reserve (ON_HOLD with expirationMinutes) → extend → confirm, or cancel." },
    { "title": "Pricing capability", "url": "https://octodocs.peek.com/capabilities/pricing", "note": "All prices are integers in minor units with currencyPrecision." },
    { "title": "Webhooks capability", "url": "https://octodocs.peek.com/capabilities/webhooks", "note": "One event, booking_update; the body carries the full booking." }
  ],
  "access": {
    "public_docs": true,
    "how_to_get_access": "Gated keys, public docs. Email Peek (ben.smithart@peek.com per the Request Access page); Peek Pro's product team issues an OCTO API key scoped to the suppliers (venues) that connect to SplitClub as a reseller. Operators' own back-office data goes through Peek's separate partner Developer Hub (in-app SDK), which is not publicly documented and is not used here.",
    "auth": "Authorization: Bearer <OCTO API key>. Invalid or deactivated keys get 403.",
    "base_url": "https://octo.peek.com/integrations/octo"
  },
  "splitclub": {
    "integration_provider": null,
    "recommended_path": "poll_and_post",
    "note": "SplitClub's inbound hook does not accept a 'peek' provider yet. Path: a SplitClub-side worker with the OCTO key lists GET /products, checks POST /availability for each product/option over a rolling window, and POSTs Offerings (inline tiers and slots) to SplitClub. Peek's reserve/confirm flow fits a group checkout well: when the party starts paying, POST /bookings with expirationMinutes covering pay_by (status ON_HOLD, utcExpiresAt), POST /bookings/{uuid}/extend if needed, POST /bookings/{uuid}/confirm with resellerReference = the checkout's external_booking_id once funded, or let it expire / POST /bookings/{uuid}/cancel if not. Register a booking_update webhook (POST /webhooks) to learn about supplier-side changes; it can go straight to SplitClub's hook once 'peek' is an accepted provider."
  },
  "objects": {
    "Offering": {
      "source": "GET /products and GET /products/{id} (with octo/pricing fields)",
      "source_url": "https://octodocs.peek.com/booking-flow/products",
      "note": "Pointers are rooted at the SplitClub Offering. Peek products have no category field and Peek's OCTO docs do not include the octo/content capability, so there is no customer-facing title, description or images: title comes from internalName ('The name this supplier calls the product'). kind is set per product at onboarding (boat_day, rental, package, ride…). Only one option per product exists today; key everything on productId + optionId anyway.",
      "fields": [
        { "to": "/external_id", "from": "id (+ options[].id)", "transform": "prefix pk_prod_; append _<optionId> when a product has more than one option", "source_url": "https://octodocs.peek.com/booking-flow/products" },
        { "to": "/kind", "from": "const:boat_day | rental | package | ride", "transform": "set per product at onboarding; not in Peek's data" },
        { "to": "/title", "from": "internalName", "transform": "truncate to 120; the supplier's internal name, review before publishing", "source_url": "https://octodocs.peek.com/booking-flow/products" },
        { "to": "/tiers", "from": "options[].units[] (pricingPer = UNIT) or the product itself (pricingPer = BOOKING)", "transform": "see PriceTier", "source_url": "https://octodocs.peek.com/capabilities/pricing" },
        { "to": "/availability", "from": "POST /availability", "transform": "see Availability", "source_url": "https://octodocs.peek.com/booking-flow/availability" },
        { "to": "/policy/cancellation", "from": "options[].cancellationCutoffAmount + options[].cancellationCutoffUnit", "transform": "unit is only 'hour' → [{hours_before: cancellationCutoffAmount, refund_pct: 100}]. The docs give the cancellation cutoff, not a refund percentage, so refund_pct 100 is an assumption; per booking, booking.cancellable is authoritative", "source_url": "https://octodocs.peek.com/booking-flow/products", "unverified": true },
        { "to": "/metadata/peek_option_id", "from": "options[].id", "source_url": "https://octodocs.peek.com/booking-flow/products" },
        { "to": "/metadata/peek_timezone", "from": "timeZone", "transform": "IANA name; also used for /availability/timezone", "source_url": "https://octodocs.peek.com/booking-flow/products" }
      ]
    },
    "PriceTier": {
      "source": "options[].units[] with units[].pricingFrom[] (pricingPer = UNIT), or product.pricingFrom[] (pricingPer = BOOKING)",
      "source_url": "https://octodocs.peek.com/capabilities/pricing",
      "note": "Pointers are rooted at the Offering; /tiers/0 is each unit (ADULT, CHILD, OTHER add-on…). Money: 'All pricing is given in integers… e.g. USD = 2 and JPY = 0': retail, original and net are integers in the currency's minor unit with currencyPrecision. pricingFrom has one entry per currency in availableCurrencies; take the one matching product.defaultCurrency. Keys ending in From are indicative; the final price comes from the availability check (unitPricing) and the booking (pricing).",
      "fields": [
        { "to": "/tiers/0/external_id", "from": "options[].units[].id", "transform": "prefix pk_unit_", "source_url": "https://octodocs.peek.com/booking-flow/products" },
        { "to": "/tiers/0/label", "from": "options[].units[].internalName (fallback units[].type)", "transform": "truncate to 80; type is ADULT | YOUTH | CHILD | INFANT | FAMILY | SENIOR | STUDENT | MILITARY | OTHER", "source_url": "https://octodocs.peek.com/booking-flow/products" },
        { "to": "/tiers/0/amount", "from": "units[].pricingFrom[].retail", "transform": "integer minor units when currencyPrecision = 2: copy as is (4500 = $45.00). If currencyPrecision differs from SplitClub's minor unit for that currency, rescale by 10^(minor − currencyPrecision). retail is 'the sale price you should charge your customers' (not net)", "source_url": "https://octodocs.peek.com/capabilities/pricing" },
        { "to": "/tiers/0/currency", "from": "units[].pricingFrom[].currency", "transform": "upper → lower case (USD → usd)", "source_url": "https://octodocs.peek.com/capabilities/pricing" },
        { "to": "/tiers/0/unit", "from": "product.pricingPer", "transform": "UNIT → per_person (per_unit for an OTHER add-on unit such as a rental item); BOOKING → per_group, one tier from product.pricingFrom", "source_url": "https://octodocs.peek.com/capabilities/pricing" },
        { "to": "/tiers/0/min_party", "from": "options[].restrictions.minUnits", "transform": "null = 0 → omit; option-level, so copy to every tier", "source_url": "https://octodocs.peek.com/booking-flow/products" },
        { "to": "/tiers/0/max_party", "from": "options[].restrictions.maxUnits", "transform": "null = unlimited → omit; clamp to 50", "source_url": "https://octodocs.peek.com/booking-flow/products" },
        { "to": "/tiers/0/fees/0", "from": "units[].pricingFrom[].includedTaxes[] {name, retail, net}", "transform": "{type: tax, label: name, amount: retail, included: true}", "source_url": "https://octodocs.peek.com/capabilities/pricing" }
      ]
    },
    "Availability": {
      "source": "POST /availability {productId, optionId, localDateStart, localDateEnd[, units]} (POST /availability/calendar is cached, per day, and has no availabilityId)",
      "source_url": "https://octodocs.peek.com/booking-flow/availability",
      "note": "Pointers are rooted at the Offering. Times: localDateTimeStart/End are 'in the local time zone to the product' and the documented examples already carry an offset (\"2020-07-01T14:30:00-08:00\"), so they pass through. utcCutoffAt is UTC with Z. For OPENING_HOURS products (allDay true, vacancies/capacity null = unlimited) there is one availability per day; SplitClub needs a capacity ≥ 1, so the venue must give a cap or the slot is skipped.",
      "fields": [
        { "to": "/availability/timezone", "from": "product.timeZone", "source_url": "https://octodocs.peek.com/booking-flow/products" },
        { "to": "/availability/slots/0/external_id", "from": "id (availability id, e.g. 20200701143000_720_…)", "transform": "keep as is with prefix pk_av_; needed as availabilityId when booking", "source_url": "https://octodocs.peek.com/booking-flow/availability" },
        { "to": "/availability/slots/0/starts_at", "from": "localDateTimeStart", "transform": "already ISO 8601 with offset", "source_url": "https://octodocs.peek.com/booking-flow/availability" },
        { "to": "/availability/slots/0/ends_at", "from": "localDateTimeEnd", "transform": "already ISO 8601 with offset", "source_url": "https://octodocs.peek.com/booking-flow/availability" },
        { "to": "/availability/slots/0/capacity", "from": "vacancies", "transform": "units left; null = unlimited (FREESALE) → venue-set cap; 0 or status SOLD_OUT / CLOSED → drop the slot", "source_url": "https://octodocs.peek.com/booking-flow/availability" },
        { "to": "/availability/slots/0/tier_overrides/0/tier", "from": "unitPricing[].unitId", "transform": "prefix pk_unit_; only returned when units are sent in the request", "source_url": "https://octodocs.peek.com/capabilities/pricing" },
        { "to": "/availability/slots/0/tier_overrides/0/amount", "from": "unitPricing[].retail", "transform": "integer minor units; only when it differs from the tier", "source_url": "https://octodocs.peek.com/capabilities/pricing" },
        { "to": "/availability/cutoff_minutes", "from": "utcCutoffAt", "transform": "minutes = localDateTimeStart − utcCutoffAt (both absolute); per slot, so use the usual value", "source_url": "https://octodocs.peek.com/booking-flow/availability" }
      ]
    },
    "Checkout": {
      "source": "Booking: POST /bookings (reserve), Get Booking (/bookings/{uuid}, listed on the page as POST), or the booking_update webhook body",
      "source_url": "https://octodocs.peek.com/booking-flow/booking",
      "note": "Pointers are rooted at checkout_create. SplitClub-first: reserve with a uuid SplitClub generates (it is the idempotency key) and expirationMinutes, create the Checkout, confirm with resellerReference = external_booking_id when funded. booking.pricing appears in the reservation response with octo/pricing, but the documented booking_update webhook example has no pricing, so read amounts from Get Booking (/bookings/{uuid}, listed on the page as POST).",
      "fields": [
        { "to": "/external_booking_id", "from": "uuid", "transform": "prefix pk_bk_ (or SplitClub's own id echoed in resellerReference)", "source_url": "https://octodocs.peek.com/booking-flow/booking" },
        { "to": "/amount", "from": "pricing.retail", "transform": "integer minor units with pricing.currencyPrecision (8800 = €88.00 at precision 2)", "source_url": "https://octodocs.peek.com/capabilities/pricing" },
        { "to": "/currency", "from": "pricing.currency", "transform": "upper → lower case", "source_url": "https://octodocs.peek.com/capabilities/pricing" },
        { "to": "/party_size", "from": "unitItems[]", "transform": "count of unit items (one per ticket); exclude OTHER add-on units; must be 2..50", "source_url": "https://octodocs.peek.com/booking-flow/booking" },
        { "to": "/offering_external_id", "from": "productId (+ optionId)", "transform": "prefix pk_prod_", "source_url": "https://octodocs.peek.com/booking-flow/booking" },
        { "to": "/pay_by", "from": "utcExpiresAt (status ON_HOLD)", "transform": "UTC with Z, already valid; pay_by must not be later than the hold, so reserve with expirationMinutes ≥ the payment window or extend via POST /bookings/{uuid}/extend", "source_url": "https://octodocs.peek.com/booking-flow/booking" },
        { "to": "/description", "from": "availability.localDateTimeStart + product internalName", "transform": "e.g. \"Sunset sail for 6, Sat 8 Nov 17:30\"", "source_url": "https://octodocs.peek.com/booking-flow/booking" },
        { "to": "/metadata/peek_supplier_reference", "from": "supplierReference", "transform": "the Peek Pro reference staff search by", "source_url": "https://octodocs.peek.com/booking-flow/booking" }
      ]
    }
  },
  "webhooks": {
    "supported": true,
    "events": [
      {
        "event": "booking_update",
        "maps_to": "status CONFIRMED → mark the linked Checkout completed; CANCELLED → cancel it; EXPIRED → expire it; otherwise update",
        "source_url": "https://octodocs.peek.com/capabilities/webhooks",
        "fields": [
          { "to": "/external_booking_id", "from": "booking.uuid", "transform": "prefix pk_bk_ (or match booking.resellerReference)", "source_url": "https://octodocs.peek.com/capabilities/webhooks" },
          { "to": "/party_size", "from": "booking.unitItems[]", "transform": "count", "source_url": "https://octodocs.peek.com/capabilities/webhooks" },
          { "to": "/offering_external_id", "from": "booking.productId", "transform": "prefix pk_prod_", "source_url": "https://octodocs.peek.com/capabilities/webhooks" }
        ]
      }
    ],
    "note": "Created by the reseller: POST /webhooks {url, event: \"booking_update\"} (list with GET /webhooks, delete with DELETE /webhooks/{id}). booking_update is 'triggered when an existing booking is confirmed, updated or cancelled'. Peek POSTs {\"webhook\": {id, event, url}, \"booking\": {…full booking…}}. No signature scheme is documented; the docs only say the url may embed basic-auth credentials (https://user:pass@example.com/webhook), so SplitClub should issue per-venue basic-auth credentials and check the Authorization header, then re-read Get Booking (/bookings/{uuid}, listed on the page as POST) before acting."
  },
  "gaps": [
    "API keys are gated (email Peek's product team); docs are public. The operator-side Developer Hub/SDK is not publicly documented and is not mapped.",
    "No customer-facing title, description or images: Peek's OCTO docs do not implement octo/content, so Offering.title is the supplier's internalName and media must come from the venue.",
    "Products have no category, so Offering.kind is set per product at onboarding.",
    "Cancellation cutoff is documented, refund percentage is not; refund_pct 100 is an assumption.",
    "OPENING_HOURS / FREESALE availabilities report vacancies null (unlimited); SplitClub needs a positive capacity from the venue.",
    "Webhooks are not signed (basic auth in the URL is the only documented protection), and only booking_update exists: no product or availability change events, so inventory must be polled.",
    "The webhook example omits booking.pricing; amounts must be re-read from Get Booking (/bookings/{uuid}, listed on the page as POST).",
    "Who collects payment (SplitClub as reseller paying net vs retail) and how Peek settles with the supplier is not in the docs; needs Peek's partner team."
  ]
}
