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
contactEmailplus aSunsetresponse header.
Endpoints
/v1/public/listingsCursor-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": "..."
}/v1/public/listings/:idFull listing detail — variants, addons, media, reviews aggregate, location, book URL.
/v1/public/operators/:slugOperator profile: name, bio, social links.
/v1/public/operators/:slug/listings.jsonDenormalized per-operator feed — every active listing with full detail. Designed for periodic aggregator polling.
/v1/public/availability/batchQuery params: listingIds[] + from + to. Returns starting price per listing + availability flag.
/v1/public/operators/:slug/calendar.icsRead-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.