← Home

Partner API

Version v1/public/*. Breaking changes bump to v2/public/*. Non-breaking additions (new fields, new endpoints, new filter params) are guaranteed within a version.

Authentication

All /v1/public/* endpoints require a bearer API key issued to your partner account.

Authorization: Bearer bp_<8char_prefix>_<secret>

The prefix is visible in server logs; the secret is never logged. Rotate keys by creating a new one and revoking the old. Keys are scoped — read is sufficient for the feed, operator detail, and availability endpoints.

Versioning policy

  • We version the URL prefix, not headers.
  • Within /v1/public/*, we will add new fields and new endpoints but never remove or rename existing ones.
  • Breaking changes bump to /v2/public/*. The older version is supported for at least 90 days after v2 launches.
  • Deprecations are announced via email to your contactEmail plus a Sunset response header.

Endpoints

GET/v1/public/listings

Cursor-paginated listings feed.

curl -H "Authorization: Bearer bp_..." \
  "https://<host>/v1/public/listings?category=fishing&limit=50"

Response

{
  "listings": [
    {
      "id": "...",
      "slug": "half-day-charter",
      "name": "Half-Day Charter",
      "category": "fishing",
      "subcategoryId": "...",
      "operator": { "id": "...", "slug": "demo", "name": "Demo Operator" },
      "detailUrl": "https://<host>/products/..."
    }
  ],
  "nextCursor": "..."
}
GET/v1/public/listings/:id

Full listing detail — variants, addons, media, reviews aggregate, location, book URL.

GET/v1/public/operators/:slug

Operator profile: name, bio, social links.

GET/v1/public/operators/:slug/listings.json

Denormalized per-operator feed — every active listing with full detail. Designed for periodic aggregator polling.

GET/v1/public/availability/batch

Query params: listingIds[] + from + to. Returns starting price per listing + availability flag.

GET/v1/public/operators/:slug/calendar.ics

Read-only iCal feed of confirmed bookings. Useful for embedding operator availability on a partner site. No auth required (ICS readers can't attach headers).

Webhooks

When a partner account is registered with a webhookUrl, we POST signed events for:

  • booking.confirmed — a booking referred by this partner is now confirmed.
  • booking.cancelled — a confirmed booking has been cancelled.
  • booking.held — hold created but not yet confirmed.
  • review.published — a new public review on an attributed booking.

Each delivery includes a signature header:

X-Partner-Signature: t=1713827100,v1=<hex_hmac_sha256>

Verify by computing HMAC-SHA256(webhookSecret, `$${t}.$${rawBody}`) and comparing in constant time. Reject if |now - t| > 300 seconds.

Backoff schedule: 1m, 5m, 30m, 2h, 12h. We give up at 24 hours — operators can retry manually from their distribution dashboard.

# Node.js verifier
import { createHmac, timingSafeEqual } from 'crypto';
const [t, v1] = header.split(',').map(p => p.split('=')[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) throw new Error('stale');
const expected = createHmac('sha256', secret)
  .update(`${t}.${rawBody}`)
  .digest('hex');
if (!timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'))) {
  throw new Error('bad signature');
}

Attribution

Deep-link into the booking flow with:

https://<host>/book/<PRODUCT_SLUG>?partner=<your_slug>&ref=<free_form>

The partner slug identifies your partner account; the ref is a free-form string you set to track placement, campaign, etc. Both get persisted on the booking row. Attribution uses last-click semantics with a 30-day cookie window.

Embeds pass the same query params through the iframe src:

<iframe
  src="https://<host>/embed/<PRODUCT_ID>?partner=<your_slug>&ref=<placement>"
  width="100%" height="800"></iframe>

Commission

Your commissionBasisPoints (set during partner onboarding — 500 = 5%) is applied to the booking total at confirm time. The resulting amount is generated as a separate payout allocation scoped to your partner account. Payouts to partners go live in a later phase alongside real Stripe Connect onboarding.

Commission comes off the platform's take, not provider payouts — adding a partner to an existing listing never reduces what the operator's providers receive.

Rate limits

Default: 600 requests per minute per API key. If you need more for batch polling, contact us. The denormalized feed /listings.json is cheap to poll every few minutes.