Warming up the decks…
Warming up the decks…
Developer docs
Sell tickets to your Blend events from your own custom-designed website: your theme, your checkout, Blend's backend (payments, tickets, emails, fraud protection). No iframe, no Blend-branded popup.
https://api.blendapp.ai/apiThe Storefront API is additive and lives under /v1/storefront/*. It delegates to the same booking and payment engine Blend's own apps use, so pricing authority, oversold protection, QR tickets, and every confirmation email behave identically.
Your custom site ──(x-blend-key)──► /v1/storefront/* ──► Blend booking engine
(themed UI) (key + domain + (pricing, capacity,
per-event opt-in) tickets, emails)
│
paid? ─────────────┘──► Stripe / Digital Wallet hosted page
(card entry is NEVER on your site)In the Blend host panel → Settings → Storefront API:
NEXT_PUBLIC_BLEND_API_URL=https://api.blendapp.ai/api/v1
NEXT_PUBLIC_BLEND_KEY=pk_live_xxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_BLEND_EVENT_IDS=<comma-separated event ids you switched on>Send your publishable key as a header on every request:
x-blend-key: pk_live_xxxxxxxxxxxxxxxxxxxxA request is accepted only when all are true:
| Key present, valid & enabled | 401 MISSING_KEY / INVALID_KEY |
| Origin/Referer host is allowlisted | 403 ORIGIN_NOT_ALLOWED |
| Event is yours & opted-in | 404 / 403 EVENT_NOT_PUBLIC |
Every response is { success, data?, message?, code? }.
Base https://api.blendapp.ai/api
/events/:eventId:eventId may be an id, shortId, or slug. Render whatever tickets[] returns: new tickets appear automatically; never hardcode prices./quote{ "eventId": "abc12345", "selectedTicketId": "t1", "quantity": 2 }
// seated: { "eventId":"abc12345", "seatLabels":["A-1","A-2"], "gaSelections":[] }
// → { "data": { "subtotal": 70, "discount": 0, "fee": 6.1, "total": 76.1, "currency": "USD" } }/register-guestquantity for multiple tickets; for reserved seats send seatLabels + seatHoldToken instead of a ticket id.{ "eventId": "abc12345", "name": "Jane Austen",
"email": "jane@x.com", "phone": "+961…",
"selectedTicketId": "t1", "quantity": 2 }Pricing is recomputed server-side: a tampered client cannot change what is charged. No host or buyer email fires here for online-paid orders; the host is notified and the ticket is issued only after the payment webhook.
/stripe/create-checkout-sessionselectedTicketId + quantity + selectedTicketAmount (unit × qty); for seats send seatLabels + seatHoldToken. Omitting quantity charges a single ticket.{ "eventId": "abc12345", "registrationId": "RG12345",
"selectedTicketId": "t1", "quantity": 2, "selectedTicketAmount": 70,
"successUrl": "https://yoursite.com/checkout/success?reg=RG12345&session_id={CHECKOUT_SESSION_ID}",
"cancelUrl": "https://yoursite.com/checkout" }
// → { "data": { "url": "https://checkout.stripe.com/c/pay/…" } }/whish/create-payment{ url } to redirect to. Send amount = the fee-inclusive total (from /quote), selectedTicketId + quantity for GA, or seatLabels + seatHoldToken for seats./registration/:registrationId// → { "data": { "status": "confirmed",
// "payment": { "paymentStatus": "paid", "amount": 50 } } }1. GET /storefront/events/:id → render tickets in your theme
2. POST /storefront/register-guest → { registrationId }
3a. free → show confirmation (already confirmed)
3b. paid → POST /storefront/stripe/create-checkout-session → redirect to { url }
4. buyer pays on Blend's hosted page; Blend's signed webhook confirms server-side
5. buyer returns to your successUrl; GET /storefront/registration/:id to display statusPayment is confirmed server-side by Blend's signed Stripe webhook / Digital Wallet callback: the success-page poll is only for display. Poll with a hard timeout; if it hasn't flipped, show “your ticket will arrive by email” (it will).
For events with a Blend seat map, the simplest integration is to embed Blend's own seat picker as an iframe: it renders the map, holds seats (in-memory with a TTL), and posts the held seats + hold token back to your page. You then thread those into the register / pay calls. No seat-rendering code on your side.
<iframe src="https://www.blendapp.ai/events/<slug>/<shortId>/seatmap?app=storefront"></iframe>
// your page ──postMessage{source:'blend-host',type:'hello'}──► iframe
// iframe ──postMessage{type:'held', seatLabels, holdToken, subtotal}──► your page
// then: register-guest / create-checkout-session with seatLabels + seatHoldTokenPrefer to build your own picker? The hold engine is also exposed directly (same key + domain + per-event gate):
/seating/events/:id/config/seating/events/:id/availability/seating/events/:id/hold-token/seating/events/:id/hold/seating/events/:id/hold/:token/extend/seating/events/:id/releaseSeats are held in memory with a timeout, never double-booked, and booked only on the payment webhook. Prices come from the seat's category server-side; unpriced seats are rejected, never sold for $0.
Every response is { success, data?, message?, code? }. Common codes:
| 401 | MISSING_KEY / INVALID_KEY | No key, or a disabled/unknown key. |
| 403 | ORIGIN_NOT_ALLOWED | Request origin isn’t on the key’s domain allowlist. |
| 403 | EVENT_NOT_PUBLIC | Event exists but isn’t switched on for the storefront. |
| 404 | EVENT_NOT_FOUND / NOT_FOUND | Not your event/registration (existence is never disclosed across tenants). |
| 400 | SEAT_NOT_PRICED | A picked seat’s category has no price: set one in the Seating tab. |
| 409 | SEAT_CONFLICT | A seat was just taken by someone else: re-pick. |
| 400 | AMOUNT_MISMATCH | Client amount ≠ server price (e.g. price changed): refresh and retry. |
| 413 | - | Body over 64 KB. |
| 429 | - | Rate limit: ~120 reads/min and ~30 writes/min per IP; payments ~20/min. |