{
  "$schema": "./_mapping.schema.json",
  "provider": "boulevard",
  "name": "Boulevard",
  "vertical": "wellness",
  "verified_on": "2026-10-13",
  "docs": [
    {
      "title": "Admin API overview (2026-06)",
      "url": "https://developers.joinblvd.com/2026-06/admin-api/overview",
      "note": "Public, no login. The portal is a single-page app; the GraphQL schema and guides ship inside its bundle. API version 2020-01 is frozen; new integrations must use 2026-06."
    },
    {
      "title": "Admin API authentication (OAuth client credentials, M2M)",
      "url": "https://developers.joinblvd.com/2026-06/admin-api/client-credentials-m2m"
    },
    {
      "title": "Admin API app registration (Enterprise-only note)",
      "url": "https://developers.joinblvd.com/2026-06/admin-api/app-registration"
    },
    {
      "title": "Using Webhooks (guide, signature)",
      "url": "https://developers.joinblvd.com/2026-06/admin-api/guides/webhooks"
    },
    {
      "title": "Webhooks reference: Appointments",
      "url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
    },
    {
      "title": "Webhooks reference: Orders",
      "url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/orders"
    },
    {
      "title": "Webhooks reference: Locations",
      "url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/locations"
    },
    {
      "title": "Admin API types: Service, Appointment, AppointmentService, Order, OrderSummary, Location, Money, DateTime",
      "url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service"
    },
    {
      "title": "Client API: querying available dates and times",
      "url": "https://developers.joinblvd.com/2026-06/client-api/guides/available-dates-and-times"
    },
    {
      "title": "Client API: booking an appointment (incl. group booking)",
      "url": "https://developers.joinblvd.com/2026-06/client-api/guides/booking-an-appointment"
    },
    {
      "title": "Client API types: CartAvailableItem, CartBookableItem",
      "url": "https://developers.joinblvd.com/2026-06/client-api/api-reference/types/CartAvailableItem"
    }
  ],
  "access": {
    "public_docs": true,
    "how_to_get_access": "API access (custom applications) is limited to Boulevard Enterprise-tier businesses. A developer account and sandbox business come from your Boulevard partner contact; the app is registered by manifest in the Developer Portal and installed by each business (offline/M2M apps install via consent and use client credentials).",
    "auth": "OAuth 2: POST https://dashboard.boulevard.io/oauth2/token (client_credentials for M2M, resource=https://dashboard.boulevard.io/api/2026-06/admin), then Authorization: Bearer <access token> (1 h). Legacy 2020-01 used Basic auth with an HMAC-signed token.",
    "base_url": "https://dashboard.boulevard.io/api/2026-06/admin (Client API: …/api/2026-06/client; sandbox: https://sandbox.joinblvd.com/api/2026-06/…)"
  },
  "splitclub": {
    "integration_provider": null,
    "recommended_path": "post_from_partner_server",
    "note": "SplitClub's inbound hook does not accept 'boulevard', and the 'custom' path only verifies Square signatures, so Boulevard's x-blvd-oauth-hmac-sha256 cannot be checked there. A relay registered as the Boulevard app receives APPOINTMENT_* webhooks, verifies them, queries the appointment node for prices, and calls POST /v1/checkouts; it also syncs services nightly into POST /v1/offerings. Pointers are rooted at the SplitClub Offering (tiers and availability inline) or checkout_create."
  },
  "objects": {
    "Offering": {
      "source": "Admin API query services (ServiceConnection → Service)",
      "kind": "spa_treatment",
      "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service",
      "fields": [
        {
          "to": "/external_id",
          "from": "Service.id",
          "transform": "global ID 'urn:blvd:Service:<uuid>' → keep, or prefix blvd_",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service"
        },
        {
          "to": "/kind",
          "from": "const:spa_treatment",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service"
        },
        {
          "to": "/title",
          "from": "Service.name",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service"
        },
        {
          "to": "/description",
          "from": "Service.description",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service"
        },
        {
          "to": "/category",
          "from": "Service.category.name",
          "transform": "map to a SplitClub category slug (SplitClub-side table)",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service"
        },
        {
          "to": "/metadata/duration_minutes",
          "from": "Service.defaultDuration",
          "transform": "integer minutes → string. Boulevard says the default is for administration; actual duration varies by location/staff",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service"
        },
        {
          "to": "/metadata/boulevard_external_id",
          "from": "Service.externalId",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Service"
        }
      ]
    },
    "PriceTier": {
      "source": "Client API Cart.availableCategories[].availableItems (CartAvailableBookableItem.listPriceRange / staffVariants[].price); Admin API Service.defaultPrice as fallback",
      "source_url": "https://developers.joinblvd.com/2026-06/client-api/api-reference/types/CartAvailableItem",
      "note": "Pointers are inside the Offering: /tiers/N. Boulevard documents Service.defaultPrice as an admin default that 'must not be used to display to customers'; the customer-facing price is the Client API listPriceRange (min/max) per location, or a staff variant's price.",
      "fields": [
        {
          "to": "/tiers/0/external_id",
          "from": "CartAvailableBookableItem.id",
          "transform": "one tier per bookable item; with staff variants use the variant's staff id as suffix",
          "source_url": "https://developers.joinblvd.com/2026-06/client-api/api-reference/types/CartAvailableItem"
        },
        {
          "to": "/tiers/0/label",
          "from": "CartAvailableBookableItem.name",
          "source_url": "https://developers.joinblvd.com/2026-06/client-api/api-reference/types/CartAvailableItem"
        },
        {
          "to": "/tiers/0/amount",
          "from": "CartAvailableBookableItem.listPriceRange.min (fallback Service.defaultPrice)",
          "transform": "Money is already integer minor units (documented: 1 USD = 100) → copy as is; when listPriceRange.variable is true, emit one tier per staffVariants[].price",
          "source_url": "https://developers.joinblvd.com/2026-06/client-api/api-reference/types/CartAvailableItem"
        },
        {
          "to": "/tiers/0/currency",
          "from": "const:usd",
          "transform": "Money says 'the applicable currency is specified separately', but no currency field was found on Business or Location",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Money",
          "unverified": true
        },
        {
          "to": "/tiers/0/unit",
          "from": "const:per_person",
          "transform": "each bookable item is one guest's service",
          "source_url": "https://developers.joinblvd.com/2026-06/client-api/api-reference/types/CartAvailableItem"
        }
      ]
    },
    "Availability": {
      "source": "Client API query cartBookableDates / cartBookableTimes for a cart holding the service (CartBookableDate, CartBookableTime)",
      "source_url": "https://developers.joinblvd.com/2026-06/client-api/guides/available-dates-and-times",
      "note": "Pointers are inside the Offering: /availability. Times exist only per cart (location + selected bookable items), so the relay creates a cart per service and polls a date range. Boulevard asks integrators to cache dates/times responsibly (fixed-cost queries).",
      "fields": [
        {
          "to": "/availability/timezone",
          "from": "Location.tz",
          "transform": "IANA name (webhook samples show 'America/Los_Angeles')",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Location"
        },
        {
          "to": "/availability/slots/0/external_id",
          "from": "CartBookableTime.id",
          "transform": "string; this is the id passed to reserveCartBookableItems",
          "source_url": "https://developers.joinblvd.com/2026-06/client-api/api-reference/types/CartBookableItem"
        },
        {
          "to": "/availability/slots/0/starts_at",
          "from": "CartBookableTime.startTime",
          "transform": "DateTime is UTC with 'Z' (documented scalar) → re-express with the Location.tz offset for that instant",
          "source_url": "https://developers.joinblvd.com/2026-06/client-api/guides/available-dates-and-times"
        },
        {
          "to": "/availability/slots/0/ends_at",
          "from": "CartBookableTime.startTime + listDurationRange.max",
          "transform": "DateTime is UTC with 'Z' (documented scalar) → re-express with the Location.tz offset for that instant; duration in minutes from CartAvailableBookableItem.listDurationRange",
          "source_url": "https://developers.joinblvd.com/2026-06/client-api/guides/available-dates-and-times"
        },
        {
          "to": "/availability/slots/0/capacity",
          "from": "const:1",
          "transform": "each bookable time is one start for the cart's items",
          "source_url": "https://developers.joinblvd.com/2026-06/client-api/guides/available-dates-and-times"
        }
      ]
    },
    "Checkout": {
      "source": "Webhook APPOINTMENT_CREATED (data.node: Appointment), enriched with Admin API node(id:) { ... on Appointment { appointmentServices { price } } }",
      "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments",
      "note": "The webhook sample has no prices, so the relay queries AppointmentService.price. A group booking (Client API Group Booking) creates one appointment per guest; group them by Cart/order into one checkout (SplitClub-side).",
      "fields": [
        {
          "to": "/external_booking_id",
          "from": "data.node.id",
          "transform": "'urn:blvd:Appointment:<uuid>'",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
        },
        {
          "to": "/offering_external_id",
          "from": "data.node.appointmentServices[0].serviceId",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
        },
        {
          "to": "/description",
          "from": "data.node.appointmentServices[].service.name",
          "transform": "join names with ' + '",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
        },
        {
          "to": "/party_size",
          "from": "const:1 per appointment",
          "transform": "sum appointments when grouping (Appointment.isGroupedAppointment)",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Appointment"
        },
        {
          "to": "/amount",
          "from": "Appointment.appointmentServices[].price (sum)",
          "transform": "Money is already integer minor units (documented: 1 USD = 100) → copy as is; price is before discounts and taxes",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/AppointmentService"
        },
        {
          "to": "/currency",
          "from": "const:usd",
          "transform": "no currency field found",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/types/Money",
          "unverified": true
        },
        {
          "to": "/pay_by",
          "from": "data.node.startAt",
          "transform": "webhook sample already carries an offset ('2023-02-06T05:50:00-08:00'); Admin API DateTime returns UTC 'Z'. Minus cutoff_minutes",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
        },
        {
          "to": "/metadata/boulevard_location_id",
          "from": "data.node.locationId",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
        },
        {
          "to": "/metadata/boulevard_client_id",
          "from": "data.node.clientId",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
        },
        {
          "to": "/metadata/boulevard_order_id",
          "from": "data.node.orderId",
          "transform": "null until checked out",
          "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
        }
      ]
    }
  },
  "webhooks": {
    "supported": true,
    "signature": {
      "header": "x-blvd-oauth-hmac-sha256 (with x-blvd-oauth-hmac-salt)",
      "algorithm": "HMAC-SHA256, standard Base64; key = base64-decoded OAuth client secret",
      "signed_payload": "'<x-blvd-oauth-hmac-salt>:<raw body>', where the salt is 'blvd-webhook-oauth-v1:<per-delivery id>:<unix timestamp>'. Legacy 2020-01 apps get x-blvd-hmac-salt / x-blvd-hmac-sha256 with salt 'blvd-webhook-v1:<API Application UUID>:<timestamp>' and the app secret as key.",
      "source_url": "https://developers.joinblvd.com/2026-06/admin-api/guides/webhooks"
    },
    "events": [
      {
        "event": "APPOINTMENT_CREATED",
        "maps_to": "create Checkout",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments",
        "fields": [
          {
            "to": "/external_booking_id",
            "from": "data.node.id",
            "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
          },
          {
            "to": "/offering_external_id",
            "from": "data.node.appointmentServices[].serviceId",
            "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
          },
          {
            "to": "/pay_by",
            "from": "data.node.startAt",
            "transform": "has offset in the sample; minus cutoff_minutes",
            "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
          },
          {
            "to": "/metadata/boulevard_location_tz",
            "from": "data.node.location.tz",
            "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
          }
        ]
      },
      {
        "event": "APPOINTMENT_UPDATED",
        "maps_to": "update Checkout description/amount (re-query prices)",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
      },
      {
        "event": "APPOINTMENT_RESCHEDULED",
        "maps_to": "update Checkout pay_by from data.node.startAt",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
      },
      {
        "event": "APPOINTMENT_CANCELLED",
        "maps_to": "cancel the open Checkout for data.node.id",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
      },
      {
        "event": "APPOINTMENT_CONFIRMED",
        "maps_to": "ignore",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
      },
      {
        "event": "APPOINTMENT_ARRIVED",
        "maps_to": "ignore",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
      },
      {
        "event": "APPOINTMENT_ACTIVE",
        "maps_to": "ignore",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
      },
      {
        "event": "APPOINTMENT_COMPLETED",
        "maps_to": "ignore",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/appointments"
      },
      {
        "event": "ORDER_COMPLETED",
        "maps_to": "reconcile: payload has only data.node.id; query Order.summary.currentTotal (Money, cents)",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/orders"
      },
      {
        "event": "ORDER_REFUND_CLOSED",
        "maps_to": "reconcile refunds",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/orders"
      },
      {
        "event": "LOCATION_UPDATED",
        "maps_to": "refresh Availability.timezone from data.node.tz",
        "source_url": "https://developers.joinblvd.com/2026-06/admin-api/api-reference/webhooks/locations"
      }
    ],
    "note": "Common fields: idempotencyKey (dedupe on it), businessId, apiApplicationId, event (e.g. 'appointment.created'), eventType, resource, webhookId, timestamp. Webhooks are created per location with the createWebhook mutation (subscriptions: [{eventType}]); OAuth apps must hold the scopes listed per event or fields arrive null. Retries default to 1 (maxRetries 0-10); 410 Gone or 8 h of failures disables the subscription."
  },
  "gaps": [
    "API access is Enterprise-tier only and needs a Boulevard partner contact; most Boulevard businesses cannot connect.",
    "Boulevard is appointment-based; no class/event schedule objects were found, so Event is not mapped.",
    "No currency field was found on Business, Location or Money; currency is assumed usd (unverified).",
    "Customer-facing prices and bookable times exist only in the Client API cart flow, per location and per staff variant; Service.defaultPrice is documented as admin-only.",
    "APPOINTMENT_* webhook payloads carry no prices; a follow-up Admin API query is required.",
    "Cancellation policy: CartSummary.cancellableBefore / estimatedCancellationFee exist per cart, but no policy table maps to /policy/cancellation.",
    "The developer portal renders client-side; field definitions were read from the GraphQL schema the portal ships (2026-06 Admin and Client)."
  ]
}