Warming up the decks…
Warming up the decks…
BlendPartner APIblendapp.aiBlend's checkout is a web page that your app shows inside a webview. Your app does not implement seat selection, pricing or ticket delivery. Blend draws all of that. What your app owns is small and precise: fetch a private link from your own backend, show it, and be ready to charge the buyer on your own payment rail when the page asks. This page describes those responsibilities without reference to any one platform; the iOS, Android and React Native pages turn them into code.
The app has exactly three jobs.
Your backend, not the app, calls Blend. The app receives a URL and nothing else, no key, no secret, no buyer record.
A webview with JavaScript and DOM storage enabled, cookies that survive navigation, and navigation that stays inside the webview for Blend's hosts.
Before the page loads, the app registers a native message handler. When the buyer taps Pay, the page posts a small charge message through it. The app tells its own backend, which charges the buyer and confirms to Blend.
Everything else, reserving inventory, confirming the order, issuing the ticket, happens between your backend and Blend, server-to-server. The app never talks to the Blend API directly and never holds Blend credentials.
Your ck_live_xxxxxxxx key and cs_live_xxxxxxxx secret arrive by email when your partnership is set up. They belong on your backend and nowhere else. An app binary can be unpacked; a secret inside it is public the moment the app ships. Nothing in the mobile integration needs them.
Your backend creates a session by telling Blend who is about to buy. Blend answers with a private, single-use link that is valid for 30 minutes.
Keyed call: send x-blend-key and x-blend-secret. Full reference on Buyer sessions.
| Body parameter | Type | Description |
|---|---|---|
| buyerName required | string | The buyer's full name. |
| buyerEmail required | string | Where the ticket is emailed. |
| buyerPhone | string | Optional. |
| Response field | Type | Description |
|---|---|---|
| success | boolean | true on success. |
| data.url | string | The link to load in the webview. Looks like https://blendapp.ai/channel/session?token=<64 hex characters>. |
| data.expiresAt | string (ISO 8601) | When the session stops working. 30 minutes after creation. |
A typical shape: the app calls a route on your backend, the name is yours, for example GET /api/blend/session, with the app's normal user authentication. Your backend looks up the signed-in user, calls Blend, and returns only url to the app.
// Runs on YOUR server. Keys come from your secret store, never from the app.
const BLEND_API = 'https://api.blendapp.ai/api/v1';
export async function mintBlendSession(buyer: {
name: string;
email: string;
phone?: string;
}): Promise<{ url: string; expiresAt: string }> {
const res = await fetch(BLEND_API + '/channel/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-blend-key': process.env.BLEND_KEY!, // ck_live_xxxxxxxx
'x-blend-secret': process.env.BLEND_SECRET!, // cs_live_xxxxxxxx
},
body: JSON.stringify({
buyerName: buyer.name,
buyerEmail: buyer.email,
buyerPhone: buyer.phone,
}),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw new Error('Blend session failed: ' + res.status);
}
// Return only what the app needs. The app receives { url } and nothing else.
return { url: json.data.url, expiresAt: json.data.expiresAt };
}These apply on every platform. Most are one line of configuration; all of them matter, because the checkout depends on a session cookie and on JavaScript, and a webview with either switched off shows a page that looks fine and cannot buy anything.
| Requirement | Type | Description |
|---|---|---|
| JavaScript enabled | required | The checkout and the payment bridge are JavaScript. Without it the page does not render. |
| DOM storage enabled | required | The page keeps transient checkout state in DOM storage. On Android this is off by default. |
| Cookies persist across navigations | required | Blend sets an httpOnly session cookie scoped to /channel. A webview that drops cookies between page loads loses the session and the buyer is sent back to the start. On Android also call CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true). |
| Do not clear cookies between screens | required | The session lasts 30 minutes. Clearing the cookie store when the checkout screen is pushed or popped ends it early. |
| Blend links stay inside the webview | required | Do not hand navigations to an external browser for blendapp.ai, checkout.blendapp.ai or tickets.blendapp.ai. The external browser has no session cookie and no payment bridge. The simplest rule: any host that is blendapp.ai or ends in .blendapp.ai loads in place. |
| Ticket links can open | required | After payment the page shows the ticket and links to it on tickets.blendapp.ai. Allow that navigation; it is Blend. |
| Payment bridge registered before load | required | The page checks for the bridge when it renders the Pay button. A bridge registered after load is not seen. See the bridge. |
| Back navigation | required | Wire the platform back gesture to webView.canGoBack() / goBack(), and only dismiss the screen when there is nothing to go back to. |
| Loading state until first paint | recommended | Show a placeholder until the first page finishes loading, so the buyer does not see a white rectangle. |
| Retry on network failure | recommended | If the first load fails, offer a retry that requests a fresh session URL from your backend. See session lifecycle for why it must be fresh. |
| Normal mobile user agent | required | Leave the webview's default user agent in place. Do not spoof a desktop UA: the seat map is touch-optimised and a desktop UA gets the pointer layout. |
| Rotation | safe | The pages are responsive. Landscape and rotation mid-checkout are fine; do not lock orientation on Blend's account. |
| Pull-to-refresh | safe | Reloading the current page is safe. Reloading the original session URL after it has been consumed is not; see below. |
When the buyer taps Pay, Blend's page sends the native host one message. The message has the same shape everywhere: a JSON object with type, orderId, amount and currency.
{
"type": "charge",
"orderId": "<string>",
"amount": 42.5,
"currency": "USD"
}| Field | Type | Description |
|---|---|---|
| type | string | Always "charge". Ignore any other value. |
| orderId | string | The order Blend just reserved for this session's buyer. This is the only field your backend acts on. |
| amount | number | Major currency units, for display on your payment sheet only. Never the amount you charge. |
| currency | string | ISO 4217, upper case. Display only. |
How the message reaches native code differs by platform. The name is fixed; the case differs between iOS and Android, and each must be exact.
| Platform | Type | Description |
|---|---|---|
| iOS. WKWebView | blendChannelPay | Register a WKScriptMessageHandler under that name on the webview's userContentController. The page calls window.webkit.messageHandlers.blendChannelPay.postMessage({…}) and the handler receives a dictionary. iOS guide. |
| Android. WebView | BlendChannelPay | Call webView.addJavascriptInterface(obj, "BlendChannelPay") where obj has a @JavascriptInterface method named postMessage(String). Android bindings accept primitives only, so the page sends JSON.stringify({…}) and the app parses the string. Requires minSdkVersion 17 or higher. Android guide. |
| React Native, react-native-webview | BlendChannelPay | Define window.BlendChannelPay with injectedJavaScriptBeforeContentLoaded so it forwards to window.ReactNativeWebView.postMessage, and read it in onMessage. This reuses the Android string contract, so one handler serves iOS and Android. React Native guide. |
The page checks for the bridge before it offers the Pay button. If neither name is present, the button shows a visible “cannot take payment here” state instead of a dead button. If you see that state in development, the name is misspelled or the handler was registered after the page loaded. Register it before you load the URL.
A webview message handler is origin-blind. It fires for a postMessage call from any script that runs in that webview. Blend's page, but also an injected ad, a compromised dependency, or anything else that ever gets to execute there. Native code cannot tell those apart from the message alone. Your app must never take the amount from the message and charge it.
Your app and backend must, in order:
Confirm that the webview delivering the message is showing a Blend page. On iOS read message.webView?.url; on Android read webView.url; in React Native read event.nativeEvent.url. If the host is not Blend's, discard the message silently. This does not make the channel authenticated, nothing about a webview bridge can, but it closes the “some other page entirely” case.
Your app sends orderId to your backend. Your backend asks Blend for that order (keyed, with your secret, see Confirming & settlement) and charges the amount Blend returns. The amount in the message exists so your payment sheet can show a number immediately, before that round-trip completes. Nothing more.
The controller or interface that carries the handler must belong to a webview that loads only Blend. A webview that also renders ads, marketing pages or any third-party content hands those pages the same ability to post a charge message. Give the checkout its own webview, its own registration, and its own lifecycle.
An orderId can only name an order that Blend itself just created for this session's buyer. The buyer's identity comes from the session cookie, never from the page. So the worst a forged message can do is point your app at the buyer's own pending order, and the amount for that order comes from Blend, not from the message.
Once your rail reports success, the app tells your backend, and your backend confirms to Blend with a signed, server-to-server call. The app does not call Blend, and the app does not need to tell the webview anything.
The page discovers the outcome on its own. After it posts the charge message it polls Blend for the order's status every 3 seconds for up to 6 minutes, then displays the ticket when the order is paid. Your app does not navigate, reload or inject anything into the webview after charging. Full reference on Confirming & settlement.
The buyer is watching a spinner for as long as it takes your backend to confirm. Call confirm the moment your rail reports success; do not batch it.
…/channel/session?token=… URL after it has been consumed is not safe. It will not restore the session. If your retry path needs a URL, ask your backend for a new one.expiresAt. If the buyer comes back to the checkout screen after it has passed, mint a fresh session rather than showing a stale page.For whoever signs off on the integration, the things that matter, in one place.
api.blendapp.ai from the app.orderId. The amount in the bridge message is only ever displayed.