{
  "$schema": "./_mapping.schema.json",
  "provider": "square",
  "name": "Square",
  "vertical": "pos",
  "verified_on": "2026-10-13",
  "docs": [
    { "title": "CatalogObject", "url": "https://developer.squareup.com/reference/square/objects/CatalogObject" },
    { "title": "CatalogItem", "url": "https://developer.squareup.com/reference/square/objects/CatalogItem" },
    { "title": "CatalogItemProductType enum", "url": "https://developer.squareup.com/reference/square/enums/CatalogItemProductType", "note": "REGULAR, APPOINTMENTS_SERVICE, FOOD_AND_BEV, EVENT, DIGITAL, DONATION, ... Connect V2 only creates REGULAR or APPOINTMENTS_SERVICE." },
    { "title": "CatalogItemVariation", "url": "https://developer.squareup.com/reference/square/objects/CatalogItemVariation" },
    { "title": "CatalogPricingType enum", "url": "https://developer.squareup.com/reference/square/enums/CatalogPricingType" },
    { "title": "CatalogImage", "url": "https://developer.squareup.com/reference/square/objects/CatalogImage" },
    { "title": "Money", "url": "https://developer.squareup.com/reference/square/objects/Money", "note": "amount: smallest denomination of currency; currency: ISO 4217, upper case (USD)." },
    { "title": "SearchCatalogObjects", "url": "https://developer.squareup.com/reference/square/catalog-api/search-catalog-objects", "note": "begin_time returns objects modified after a timestamp: the re-sync after catalog.version.updated." },
    { "title": "Location", "url": "https://developer.squareup.com/reference/square/objects/Location", "note": "timezone: IANA identifier." },
    { "title": "SearchAvailability (POST /v2/bookings/availability/search)", "url": "https://developer.squareup.com/reference/square/bookings-api/search-availability" },
    { "title": "Availability", "url": "https://developer.squareup.com/reference/square/objects/Availability" },
    { "title": "AppointmentSegment", "url": "https://developer.squareup.com/reference/square/objects/AppointmentSegment" },
    { "title": "Booking", "url": "https://developer.squareup.com/reference/square/objects/Booking" },
    { "title": "BookingStatus enum", "url": "https://developer.squareup.com/reference/square/enums/BookingStatus" },
    { "title": "Order", "url": "https://developer.squareup.com/reference/square/objects/Order" },
    { "title": "OrderLineItem", "url": "https://developer.squareup.com/reference/square/objects/OrderLineItem" },
    { "title": "OrderState enum", "url": "https://developer.squareup.com/reference/square/enums/OrderState" },
    { "title": "booking.created webhook", "url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.created" },
    { "title": "booking.updated webhook", "url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.updated" },
    { "title": "order.created webhook", "url": "https://developer.squareup.com/reference/square/orders-api/webhooks/order.created" },
    { "title": "order.updated webhook", "url": "https://developer.squareup.com/reference/square/orders-api/webhooks/order.updated" },
    { "title": "catalog.version.updated webhook", "url": "https://developer.squareup.com/reference/square/catalog-api/webhooks/catalog.version.updated" },
    { "title": "Verify and validate an event notification", "url": "https://developer.squareup.com/docs/webhooks/step3validate" },
    { "title": "OAuth API overview", "url": "https://developer.squareup.com/docs/oauth-api/overview" },
    { "title": "Access tokens", "url": "https://developer.squareup.com/docs/build-basics/access-tokens" }
  ],
  "access": {
    "public_docs": true,
    "how_to_get_access": "Create an application in the Square Developer Console. A seller (the venue) grants it access through Square's OAuth 2 code or PKCE flow; the access token then calls the Catalog, Bookings and Orders APIs for that seller. Webhook subscriptions belong to the developer application and the Webhook Subscriptions API needs the developer's personal access token, not an OAuth token.",
    "auth": "Authorization: Bearer <access token> plus a Square-Version header (e.g. 2026-09-16).",
    "base_url": "https://connect.squareup.com/v2 (sandbox: https://connect.squareupsandbox.com/v2)"
  },
  "splitclub": {
    "integration_provider": "square",
    "recommended_path": "webhook",
    "note": "POST /v1/integrations with provider \"square\" mints https://splitclubapp.com/api/v1/hooks/square/sc_hook_…. Register that exact URL as the notification URL of a Square webhook subscription and give the integration the subscription's signature key: partner-api's squareSignatureOk recomputes the HMAC over that URL + raw body, so any difference in the URL (trailing slash, host) fails every delivery. Square's notifications are thin (order.* carry only order_id/state/version; catalog.version.updated only a timestamp), so mapping needs an API pull with the venue's OAuth token after each delivery. Availability has no webhook at all and must be polled via SearchAvailability. The hook dedupes on type + event_id, both top-level in Square's payload."
  },
  "objects": {
    "Offering": {
      "source": "CatalogObject with type ITEM (item_data: CatalogItem), from SearchCatalogObjects / ListCatalog",
      "source_url": "https://developer.squareup.com/reference/square/objects/CatalogItem",
      "note": "One Square ITEM is one Offering; its ITEM_VARIATIONs become the Offering's tiers (see PriceTier). Use items with product_type APPOINTMENTS_SERVICE (bookable services, which have availability) or REGULAR / FOOD_AND_BEV (sold without a time slot). Skip is_archived items. Square has no Offering.kind; the venue chooses it when connecting.",
      "fields": [
        { "to": "/external_id", "from": "id", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogObject", "transform": "Square's CatalogObject id, stable once created." },
        { "to": "/title", "from": "item_data.name", "transform": "Truncate to 120 characters (Square allows 512).", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogItem" },
        { "to": "/description", "from": "item_data.description_html", "transform": "Strip HTML to plain text, max 4000 characters. item_data.description is deprecated (2022-07-20) but kept in sync.", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogItem" },
        { "to": "/kind", "from": "item_data.product_type", "transform": "APPOINTMENTS_SERVICE → the venue's choice among spa_treatment, class, rental, package (default spa_treatment); FOOD_AND_BEV → dining; REGULAR → the venue's choice. SplitClub decision; Square only documents the enum.", "source_url": "https://developer.squareup.com/reference/square/enums/CatalogItemProductType" },
        { "to": "/media/0/url", "from": "item_data.image_ids[]", "transform": "Each id is a CatalogObject of type IMAGE; read its image_data.url (Square-hosted). image_ids is marked Beta.", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogImage" },
        { "to": "/media/0/alt", "from": "image_data.caption", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogImage" },
        { "to": "/availability/timezone", "from": "Location.timezone", "transform": "IANA id of the location the item is sold at (GET /v2/locations/{id}).", "source_url": "https://developer.squareup.com/reference/square/objects/Location" },
        { "to": "/metadata/square_updated_at", "from": "updated_at", "transform": "RFC 3339 string, kept for change detection.", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogObject" }
      ]
    },
    "PriceTier": {
      "source": "CatalogObject with type ITEM_VARIATION (item_variation_data: CatalogItemVariation), nested in item_data.variations[]",
      "source_url": "https://developer.squareup.com/reference/square/objects/CatalogItemVariation",
      "note": "Sent inline as the Offering's /tiers. Only FIXED_PRICING variations carry price_money; VARIABLE_PRICING (price entered at sale) has no price to map and must be priced by hand. location_overrides[] can carry a per-location price; prefer it over price_money for the venue's location.",
      "fields": [
        { "to": "/tiers/0/external_id", "from": "item_data.variations[].id", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogObject" },
        { "to": "/tiers/0/label", "from": "item_data.variations[].item_variation_data.name", "transform": "Truncate to 80 characters.", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogItemVariation" },
        { "to": "/tiers/0/amount", "from": "item_data.variations[].item_variation_data.price_money.amount", "transform": "Already integer smallest denomination (cents for USD): copy as-is. Only when pricing_type is FIXED_PRICING.", "source_url": "https://developer.squareup.com/reference/square/objects/Money" },
        { "to": "/tiers/0/currency", "from": "item_data.variations[].item_variation_data.price_money.currency", "transform": "ISO 4217 upper case → lower case (USD → usd).", "source_url": "https://developer.squareup.com/reference/square/objects/Money" },
        { "to": "/tiers/0/unit", "from": "const:per_unit", "transform": "Square prices one item or one appointment; it has no per-person/per-group notion. SplitClub default, not from Square's docs." },
        { "to": "/availability/recurring/0/duration_minutes", "from": "item_data.variations[].item_variation_data.service_duration", "transform": "Milliseconds → minutes (÷ 60000). Only for APPOINTMENTS_SERVICE items.", "source_url": "https://developer.squareup.com/reference/square/objects/CatalogItemVariation" }
      ]
    },
    "Availability": {
      "source": "POST /v2/bookings/availability/search → availabilities[] (Availability)",
      "source_url": "https://developer.squareup.com/reference/square/bookings-api/search-availability",
      "note": "Only for APPOINTMENTS_SERVICE items. Query with filter.start_at_range, location_id and segment_filters[].service_variation_id (the tier's variation id). Each result is one bookable appointment start for one team member. Poll it: Square sends no availability webhook.",
      "fields": [
        { "to": "/availability/slots/0/starts_at", "from": "availabilities[].start_at", "transform": "RFC 3339 (Square returns UTC, e.g. 2020-11-26T13:00:00Z); re-express in the location's timezone with its offset.", "source_url": "https://developer.squareup.com/reference/square/objects/Availability" },
        { "to": "/availability/slots/0/ends_at", "from": "availabilities[].appointment_segments[].duration_minutes", "transform": "start_at + sum of segment duration_minutes, same offset.", "source_url": "https://developer.squareup.com/reference/square/objects/AppointmentSegment" },
        { "to": "/availability/slots/0/capacity", "from": "availabilities[] grouped by start_at", "transform": "Count of availabilities sharing one start_at (one per free team member). Square does not document a capacity or party-size figure; this count is SplitClub's inference.", "unverified": true },
        { "to": "/availability/timezone", "from": "Location.timezone", "transform": "Location from availabilities[].location_id.", "source_url": "https://developer.squareup.com/reference/square/objects/Location" }
      ]
    },
    "Checkout": {
      "source": "Booking (booking.created / booking.updated, or GET /v2/bookings/{id}); or Order (GET /v2/orders/{order_id} after order.created / order.updated)",
      "source_url": "https://developer.squareup.com/reference/square/objects/Booking",
      "note": "A Square Booking carries no price and no party size: the amount comes from the booked variation's price_money and party_size must be asked of the venue or guest. An Order carries total_money and is the better source when the venue opens an order for the group; split only OPEN orders (COMPLETED orders are already fully paid). Every created Checkout still needs a return_url on the venue's registered origins, which Square cannot supply.",
      "fields": [
        { "to": "/external_booking_id", "from": "booking.id | order.id", "transform": "Prefix with the source: sq_booking_<id> or sq_order_<id>.", "source_url": "https://developer.squareup.com/reference/square/objects/Booking" },
        { "to": "/offering_external_id", "from": "booking.appointment_segments[0].service_variation_id | order.line_items[0].catalog_object_id", "transform": "Both are ITEM_VARIATION ids; map to the parent item via item_variation_data.item_id.", "source_url": "https://developer.squareup.com/reference/square/objects/AppointmentSegment" },
        { "to": "/amount", "from": "order.total_money.amount", "transform": "Integer smallest denomination: copy. For a booking without an order: sum of the booked variations' price_money.amount.", "source_url": "https://developer.squareup.com/reference/square/objects/Order" },
        { "to": "/currency", "from": "order.total_money.currency", "transform": "Upper → lower case.", "source_url": "https://developer.squareup.com/reference/square/objects/Money" },
        { "to": "/pay_by", "from": "booking.start_at", "transform": "start_at minus the Offering's availability.cutoff_minutes, in the location's offset. SplitClub rule; Square has no payment deadline.", "source_url": "https://developer.squareup.com/reference/square/objects/Booking" },
        { "to": "/description", "from": "item_data.name + booking.start_at", "transform": "e.g. \"Couples massage, Sat 8:00\"; max 200 characters." },
        { "to": "/metadata/square_location_id", "from": "booking.location_id | order.location_id", "source_url": "https://developer.squareup.com/reference/square/objects/Order" },
        { "to": "/metadata/square_reference_id", "from": "order.reference_id", "transform": "Client-specified id linking the order to another system (max 40).", "source_url": "https://developer.squareup.com/reference/square/objects/Order" }
      ]
    }
  },
  "webhooks": {
    "supported": true,
    "signature": {
      "header": "x-square-hmacsha256-signature",
      "algorithm": "HMAC-SHA-256 with the subscription's signature key, compared in constant time; base64 output (Square's SDK helper; the prose does not name the encoding, and partner-api's squareSignatureOk compares base64)",
      "signed_payload": "The subscription's notification URL, exactly as registered, immediately followed by the raw request body",
      "source_url": "https://developer.squareup.com/docs/webhooks/step3validate"
    },
    "events": [
      {
        "event": "booking.created",
        "maps_to": "candidate Checkout for staff (ops_partner_inbound); amount from the booked variation's price",
        "source_url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.created",
        "fields": [
          { "to": "/external_booking_id", "from": "data.object.booking.id", "source_url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.created" },
          { "to": "/offering_external_id", "from": "data.object.booking.appointment_segments[].service_variation_id", "transform": "Variation id → parent item id.", "source_url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.created" },
          { "to": "/pay_by", "from": "data.object.booking.start_at", "transform": "start_at − cutoff_minutes.", "source_url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.created" },
          { "to": "/metadata/square_location_id", "from": "data.object.booking.location_id", "source_url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.created" }
        ]
      },
      {
        "event": "booking.updated",
        "maps_to": "status CANCELLED_BY_CUSTOMER / CANCELLED_BY_SELLER / DECLINED → cancel the linked Checkout; a changed start_at → move pay_by; ACCEPTED / PENDING / NO_SHOW → no change",
        "source_url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.updated",
        "fields": [
          { "to": "/external_booking_id", "from": "data.object.booking.id", "source_url": "https://developer.squareup.com/reference/square/bookings-api/webhooks/booking.updated" },
          { "to": "/metadata/square_booking_status", "from": "data.object.booking.status", "source_url": "https://developer.squareup.com/reference/square/enums/BookingStatus" }
        ]
      },
      {
        "event": "order.created",
        "maps_to": "GET /v2/orders/{data.object.order_created.order_id}, then a candidate Checkout when state is OPEN",
        "source_url": "https://developer.squareup.com/reference/square/orders-api/webhooks/order.created",
        "fields": [
          { "to": "/external_booking_id", "from": "data.object.order_created.order_id", "source_url": "https://developer.squareup.com/reference/square/orders-api/webhooks/order.created" },
          { "to": "/metadata/square_location_id", "from": "data.object.order_created.location_id", "source_url": "https://developer.squareup.com/reference/square/orders-api/webhooks/order.created" }
        ]
      },
      {
        "event": "order.updated",
        "maps_to": "state CANCELED → cancel the linked Checkout; COMPLETED → paid at the venue, close it; OPEN → re-read the order and update the amount while no share is held",
        "source_url": "https://developer.squareup.com/reference/square/orders-api/webhooks/order.updated",
        "fields": [
          { "to": "/external_booking_id", "from": "data.object.order_updated.order_id", "source_url": "https://developer.squareup.com/reference/square/orders-api/webhooks/order.updated" },
          { "to": "/metadata/square_order_state", "from": "data.object.order_updated.state", "source_url": "https://developer.squareup.com/reference/square/enums/OrderState" }
        ]
      },
      {
        "event": "catalog.version.updated",
        "maps_to": "re-sync Offerings: SearchCatalogObjects with begin_time = last sync, re-upsert changed ITEMs and their variations",
        "source_url": "https://developer.squareup.com/reference/square/catalog-api/webhooks/catalog.version.updated",
        "fields": [
          { "to": "/metadata/square_updated_at", "from": "data.object.catalog_version.updated_at", "transform": "Only a timestamp; the payload names no changed object.", "source_url": "https://developer.squareup.com/reference/square/catalog-api/webhooks/catalog.version.updated" }
        ]
      }
    ],
    "note": "Envelope for every event: { merchant_id, type, event_id, created_at, data: { type, id, object: {...} } } (booking events also carry location_id). partner-api stores the delivery verbatim, dedupes on type + event_id and verifies the signature when the integration holds the signature key. It maps nothing yet: these payloads wait in ops_partner_inbound for staff or a mapper built from this file."
  },
  "gaps": [
    "Square Bookings have no party size and no price: party_size for a Checkout must come from the venue or guest, and the amount from the variation's price_money (or an Order).",
    "SearchAvailability returns one appointment start per free team member; Square documents no capacity figure, so Availability capacity is an inferred count (marked unverified).",
    "No availability webhook: Availability must be polled with POST /v2/bookings/availability/search.",
    "order.* and catalog.version.updated notifications are thin (ids, state, timestamp); every mapping needs a follow-up API call with the seller's OAuth token, which partner-api's hook does not hold today.",
    "VARIABLE_PRICING variations have no price to map.",
    "Square's EVENT product type exists in CatalogItemProductType, but its date/time fields were not verified, so nothing maps to SplitClub Event.",
    "Square has no cancellation-policy, minimum-age or dress-code fields in the objects read; Offering.policy has no Square source.",
    "Webhook subscriptions belong to a developer application (Developer Console, or the Webhook Subscriptions API with a personal access token). A venue must create one pointing at its SplitClub hook URL, or SplitClub must run one app-level subscription; the per-venue URL model needs the former."
  ]
}
