BlendPartner API
blendapp.ai

Getting started

OverviewQuickstartHow the integration worksAuthenticationAny backend

Catalog

Listing eventsEvent detail & ticketsWhich events you see

Checkout

Buyer sessionsInventory & seat holdsReserving an orderTaking the paymentConfirming & settlement

Mobile apps

Native UI (no webview)Embedding the webviewiOS (Swift)Android (Kotlin)React Native

Reference

ErrorsRate limitsTesting & go-live

Taking the payment

Blend draws the checkout inside your app, but Blend never touches the buyer's money. When the buyer taps Pay, the web page hands a small note to your native app, “order such-and-such, this much” and your app charges the buyer on your own rail. This page shows how that hand-off works on iOS and Android, and, more importantly, why your app must treat that note as a hint and not as an instruction.

The sequence

  1. The page reserves the order

    Blend's checkout page calls POST /v1/channel/session/events/:id/checkout and receives an orderId, amount and currency.

  2. The page signals your native host

    It posts a charge message through the platform bridge described below.

  3. Your native app charges the buyer

    After re-fetching the authoritative amount from Blend server-to-server. See the trust model.

  4. Your backend confirms to Blend

    POST /v1/channel/orders/confirm, signed. Blend claims inventory and issues the ticket.

  5. The page polls until the order resolves

    No callback into the webview is needed. The page asks Blend for the order's status every few seconds and shows the ticket when it is paid.

The bridge

The message has the same shape on both platforms: a JSON object with type, orderId, amount and currency.

Message
{
  "type": "charge",
  "orderId": "…",
  "amount": 42.5,
  "currency": "USD"
}

iOS. WKWebView

Register a WKScriptMessageHandler named exactly blendChannelPay on the web view's user content controller. The page calls:

What the page calls
window.webkit.messageHandlers.blendChannelPay.postMessage({
  type: 'charge',
  orderId,
  amount,
  currency,
});
Receiving it
import WebKit

final class BlendCheckoutController: UIViewController, WKScriptMessageHandler {
  private let expectedHost = "blendapp.ai"
  private var webView: WKWebView!

  override func viewDidLoad() {
    super.viewDidLoad()
    let config = WKWebViewConfiguration()
    // The name must be exactly "blendChannelPay".
    config.userContentController.add(self, name: "blendChannelPay")
    webView = WKWebView(frame: view.bounds, configuration: config)
    view.addSubview(webView)
  }

  func userContentController(_ controller: WKUserContentController,
                             didReceive message: WKScriptMessage) {
    guard message.name == "blendChannelPay" else { return }

    // 1. Origin check: only act on messages from the Blend page.
    guard message.frameInfo.securityOrigin.host == expectedHost else { return }

    // 2. Read the hint. Only orderId is trusted from here.
    guard let body = message.body as? [String: Any],
          body["type"] as? String == "charge",
          let orderId = body["orderId"] as? String else { return }

    // 3. Ask YOUR backend to fetch the authoritative amount from Blend
    //    and charge that. Never charge body["amount"].
    PaymentService.shared.chargeBlendOrder(orderId: orderId)
  }

  deinit {
    webView?.configuration.userContentController
      .removeScriptMessageHandler(forName: "blendChannelPay")
  }
}

Android. WebView

Register a JavaScript interface named exactly BlendChannelPay exposing a postMessage(String) method. Android bindings only accept primitives, so the page sends the same object as a JSON string. The contract is otherwise identical.

What the page calls
window.BlendChannelPay.postMessage(JSON.stringify({
  type: 'charge',
  orderId,
  amount,
  currency,
}));
Receiving it
import android.graphics.Bitmap
import android.net.Uri
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import org.json.JSONObject

class BlendChannelPayBridge(
  private val onCharge: (orderId: String) -> Unit,
) {
  private val expectedHost = "blendapp.ai"

  // @JavascriptInterface methods run on a background thread, and WebView.url
  // may only be read on the UI thread, so the host is recorded from the
  // WebViewClient as pages load, and checked here.
  @Volatile var currentHost: String? = null

  @JavascriptInterface
  fun postMessage(raw: String) {
    // 1. Origin check: only act while the Blend page is the one loaded.
    if (currentHost != expectedHost) return

    // 2. Read the hint. Only orderId is trusted from here.
    val msg = runCatching { JSONObject(raw) }.getOrNull() ?: return
    if (msg.optString("type") != "charge") return
    val orderId = msg.optString("orderId").takeIf { it.isNotBlank() } ?: return

    // 3. Ask YOUR backend to fetch the authoritative amount from Blend
    //    and charge that. Never charge msg["amount"].
    onCharge(orderId)
  }
}

// Registration, the name must be exactly "BlendChannelPay".
val bridge = BlendChannelPayBridge(::chargeBlendOrder)
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(bridge, "BlendChannelPay")
webView.webViewClient = object : WebViewClient() {
  override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
    bridge.currentHost = Uri.parse(url).host
  }
}
If neither bridge is present

The Pay button shows a visible “can't take payment here” state instead of a dead button. If you see it during development, the handler name is wrong or the interface was not registered before the page loaded.

Powered byBlend

The trust model

The bridge message is a display hint, not a charge instruction

A webview message handler is origin-blind. Any script running in that webview, an injected ad, a compromised dependency, a man-in-the-middle on an unpinned connection, can post a message with the same shape. Your app must never take the amount in that message and charge it.

Your native code and backend must, in order:

  1. Origin-check the message

    Before acting, verify the message came from the Blend host you loaded. On iOS, use message.frameInfo.securityOrigin. On Android, check the web view's current URL host. Discard anything else silently.

  2. Re-fetch the authoritative amount server-to-server, and charge that

    Your backend calls POST /v1/channel/orders/status with the orderId (keyed, with your secret) and charges the amount it returns. That is the same number orders/confirm validates against, so a charge based on it can never trip AMOUNT_MISMATCH.

    The amount in the bridge message exists so your UI can show something immediately, nothing more.

  3. Never share the handler with non-Blend content

    The WKUserContentController or addJavascriptInterface registration must belong to a webview that loads only Blend. A webview that also renders third-party pages would give those pages the same bridge.

orderId is the real authorization boundary

An orderId can only name an order Blend just created for that session's buyer. A forged message can, at worst, point at the buyer's own pending order, and the amount for that order is fetched from Blend, not from the message. Blend's confirm endpoint then rejects any amount that does not match the order, and refuses any order that is not yours.

How the page learns the outcome

There is no callback from your app back into the webview. After posting the charge message, the page polls Blend for the order's status:

POST/v1/channel/session/orders/status
  • Every 3 seconds, for up to 6 minutes.
  • Runs inside the buyer session, authenticated by the session cookie.
  • Stops as soon as the order is paid or failed.

One polling loop covers every case. If your backend's confirm is a few seconds late, the page simply sees pending until it flips to paid. If the buyer cancelled on your payment sheet, the order never becomes paid and the page can say so. “Buyer cancelled” and “paid, but confirm is late” look different to the buyer because the loop waits long enough for a real confirm to arrive.

Confirm promptly

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.

Next

Confirming & settlementSign and send the confirm call from your backend.iOS (Swift)Full WKWebView setup for the Blend checkout.Android (Kotlin)Full WebView setup for the Blend checkout.Reserving an orderWhere orderId and amount come from.
Powered byBlend© 2026 Blend · Ticketing & event infrastructure