A session comes back with two URLs, and they are not interchangeable:
Framing url produces a blank box and one console line
(frame-ancestors 'none'), with no request reaching us and nothing in your
logs. If an embed is blank, check which of the two you used first.
Embedding keeps the payer on your site. Doing that alone works fine — the page
can connect its own wallet.
To match the page to your brand, pass theme parameters on the
URL — colours, font, and corner radius, no CSS.
Bridging a connected wallet
If your app has already connected a wallet, making the payer connect a
second time inside the iframe is a real drop-off point. The parent bridge lends
your existing connection to the frame: the iframe asks for signatures, your
provider produces them, and the payer never reconnects.
Call bridge.stop() when the iframe unmounts, or the listener outlives it.
onPaymentComplete means the payer submitted a transaction — not that
funds have settled. Cross-chain routes still have a bridge leg to finish.
Release goods on the webhook plus a session read, never on
this callback. It is a UI signal, not a settlement signal.
Keeping the bridge in sync
The bridge is constructed with an address and a chain id. If your user switches
either, tear the bridge down and start a new one — otherwise the frame keeps
signing against the wallet state you handed it at mount:
Security
Messages are origin-checked in both directions — the parent validates the
frame’s origin against an allowlist before acting on anything it says, and the
iframe does the same for the parent. That is why apiBase has a hard-coded
default: a bridge pointed at an arbitrary origin is a phishing surface, so
overriding it is deliberate rather than convenient.
Never pass a secret key here. The bridge is browser code; sessions are created
on your server.