Guides
Popup timing
Open the checkout popup synchronously, inside the click handler that triggered it. An await before the call is enough to get it blocked.
This guide picks up where the quickstart's hand-your-user-a-deposit-address step leaves off. It applies to the popup surface. To render checkout inside your own page instead, see embed checkout inline.
Why this matters
Every modern browser blocks window.open() unless it runs synchronously within a real user gesture, such as a click or a key press. Do anything asynchronous first, such as awaiting your own server call to create the payment intent, and the browser stops treating the resulting call as part of that gesture.
The browser does not throw or warn. It blocks the popup silently. This is standard popup-blocker behavior, and every checkout integration works around it the same way.
The correct pattern
Open a blank window synchronously inside the click handler, then hand it to the SDK's handle.openNow() to navigate it once the mount is ready:
payButton.addEventListener("click", () => {
const win = window.open("", "canopy_checkout", "width=460,height=640");
handle.openNow(win); // navigates the already-open window
});openNow() is what navigates the window to the checkout URL, and it can run after an await, because the window is already open by then. The window has to open before anything asynchronous happens. The navigation does not.
What happens if you get it wrong
A window opened outside a synchronous gesture comes back as null; the browser does not throw. Passing null to openNow() is handled, and it emits an error event with a code naming the popup as blocked.
By then the payer has already clicked a button that visibly did nothing, which reads worse than an explicit error state. Call window.open() as the first statement inside the click handler.
With surface: auto, the SDK handles this for its own fallback button: the fallback is offered and opened from a real click on that button.
Events from the popup
The popup reports back to the window that opened it, so ready and error reach your onEvent handler on this surface as well as on the iframe one. It targets a specific origin -- never a wildcard -- so it has to know which origin yours is.
It learns that from your embed origins if you registered any, and otherwise from the referrer your page sends when it opens the window.
A page serving Referrer-Policy: no-referrer that has registered no embed origin sends neither, and the popup then has no origin it is allowed to name: it stays silent, and onEvent sees nothing from it.
If that is your setup, register an embed origin on the dashboard -- the popup surface does not otherwise require one -- or relax the policy for this page.
resize is an iframe-only event: a popup is its own window and sizes itself.
See also
- Quickstart: the full walkthrough this guide hangs off.