Hosted checkout that meets your app where it is.
Checkout is the modern Octany checkout surface. Three ways to reach it: a plain URL, an embeddable cart, or a JS API. All three converge on the same hosted page at checkout.octany.com — Octany owns cart, customer, payment method, SCA, and the success page.
subscription.created webhook to activate them. That's the canonical pattern. Everything below is variations on it.
§ 02.1 The hosted URL #
The simplest integration. Construct a URL on your server and link or redirect to it. No JavaScript required.
https://checkout.octany.com/checkout/{account-uuid}/product/{product-id} ?email=anna@example.com &referenceId=user_42 &successUrl=https://app.example.com/welcome
Octany creates a checkout session, adds the product to its cart, and redirects to /pay/checkout/{account}/{anonymousId} where the SPA takes over. The user never leaves Octany until they finish or abandon.
https://checkout.octany.com. Use this as the canonical checkout host unless your Octany contact has provided a dedicated environment.
§ 02.2 Query parameters #
The product itself is the only required input. Everything else is opt-in pre-fill or correlation metadata.
| Param | Purpose |
|---|---|
| successUrl | Where Octany redirects after a successful payment. Must be HTTPS. |
| referenceId | Use this to correlate. Your ID for the visitor, member, donor, or order. Surfaces on the resulting Subscription / Order and on every webhook for that resource. |
| referenceName | Human-readable label that shows up in the admin UI alongside the resource. |
| Pre-fills the customer email field. | |
| firstName, lastName | Pre-fills name fields. |
| companyName | Pre-fills business-customer fields. |
| role | Single role to attach to the user (CRM segmentation). |
| roles | Comma-separated roles, e.g. roles=member,donor. |
| amount | Integer in öre/cents. Overrides the product's price for this session — for "name your price" / top-up flows. |
| locale | sv, en, etc. Defaults to the account locale. Overridable later in the UI. |
| utm_* | utm_source, utm_medium, utm_campaign, utm_content, utm_term — captured into the cart so they appear on the order. |
Minimal example
<a href="https://checkout.octany.com/checkout/{account}/product/42?referenceId=user_99&email=anna@example.com&successUrl=https://app.example.com/welcome"> Subscribe — 99 SEK / month </a>
§ 02.3 Wiring success → your app #
Don't trust query params on successUrl for state changes. Octany redirects the user there after they acknowledge payment, but the source of truth is the webhook (order.paid, subscription.created).
The robust flow:
- User lands on your
successUrl. - Show a "Thanks, finishing up…" page.
- Wait for the corresponding webhook on your backend (correlate via
reference_id). - Then activate the user / unlock the feature.
Post-checkout reconciliation: land the user back in your app instantly
The webhook is the source of truth for "this payment cleared," but it can lag a few seconds — and when your successUrl is a deep link back into a desktop or mobile app, you want the user to land now, not after a webhook round-trip. Two options exist; the second is what we recommend when latency or webhook delivery is a concern:
- Poll your own backend on the success page. Works, but races the webhook — if your webhook handler hasn't run yet, your DB still says "no subscription" and the user is stuck on the spinner.
- Poll Octany directly on the success page using
GET /subscriptions?filter[reference_id]=<your_user_id>. Finds the subscription as soon as it exists, regardless of webhook timing. Sync into your local cache and then redirect (web → next page, native app → custom URL scheme likemyapp://billing/success).
// Success page — poll Octany every 2.5s until the subscription is active app.get('/billing/pending/status', async (req, res) => { const subs = await octany.subscriptions.find({ reference_id: req.user.id }) const sub = subs[0] if (sub && ['active', 'trialing'].includes(sub.status)) { await syncSubscriptionLocally(req.user, sub) // same syncer the webhook calls return res.json({ ready: true, redirect: 'myapp://billing/success' }) } res.json({ ready: false }) })
The webhook handler should run the same syncer so both paths converge on identical local state — whichever arrives first wins, the other is a no-op. We've verified this pattern by blocking webhook deliveries for ten minutes; the user still landed back in the app within seconds of the polled subscription appearing.
§ 02.4 Embeddable cart #
For multi-product carts on a content site. Ships a loader script that injects two iframes — a floating "fab" button and a slide-out cart container — and exposes the window.Octany JS API.
Install
<!-- 1. Configure --> <script> window.OctanyConfig = { id: '{account-uuid}', locale: 'en', // optional; defaults to account locale } </script> <!-- 2. Load the loader --> <script src="https://embed.octany.com/loader.js" async></script>
The loader fetches the current cart-app version internally (from app.octany.com/pay/embed-version) and pulls the matching app.js / app.css from embed.octany.com, so your page can stay on the stable loader URL while Octany ships new versions.
§ 02.5
window.Octany JS API
#
Once the loader has booted (listen for the octany-cart-ready event on document if you need to gate calls), window.Octany is available.
// Pass through reference IDs / UTM that should land on the order Octany.set({ referenceId: 'user_42', referenceName: 'Anna Persson', email: 'anna@example.com', }) // Direct checkout — same as the URL above, but constructed for you Octany.checkout(productId, { successUrl: 'https://app.example.com/welcome', amount: 19900, // optional, name-your-price in öre }) // Cart-driven flow Octany.cart.add(productId) // add to floating cart Octany.cart.open() // open the cart container Octany.cart.close() Octany.cart.hide() // hide the floating "fab" button Octany.cart.checkout() // go to checkout with cart contents Octany.cart.requestCheckoutUrl() // → Promise<url>, no redirect
DOM events
The cart dispatches custom events on document:
| Event | When | event.detail |
|---|---|---|
| octany-cart-updated | Items added/removed/qty changed | { cart: { items, total, ... } } |
Use these to update your own page chrome (cart count badge, etc.) without poking the iframe directly.
UTM auto-capture
The embed reads utm_source/medium/campaign/content/term from the host page's URL on load and forwards them through Octany.set(...) so they land on the resulting order's utm_parameters. You don't have to wire this manually.
What's not on window.Octany
There is intentionally no Octany.portal() or Octany.manage(). Subscription management — update card, view invoices, cancel — happens via the signed billing-portal URLs returned on the subscription resource itself: update_card_url, invoices_url, and cancel_url. To send a logged-in user to "manage subscription", fetch their subscription server-side (GET /subscriptions?filter[reference_id]=…), then redirect to the URL you want. There is no "give me a fresh portal URL for this customer" endpoint — the URLs are part of the subscription payload.
§ 02.6 End-to-end example #
Paywall pattern: hosted checkout + webhook activation.
// 1. Server-side: build the URL when the user clicks "Subscribe" app.post('/subscribe', (req, res) => { const params = new URLSearchParams({ referenceId: req.user.id, email: req.user.email, successUrl: 'https://app.example.com/subscribed', }) res.redirect(`https://checkout.octany.com/checkout/${ACCOUNT}/product/${PRODUCT_ID}?${params}`) }) // 2. Webhook handler activates the user once the payment lands app.post('/octany/webhook', /* signature verification — see Webhooks page */ (req, res) => { const event = req.body if (event.name === 'subscription.created' && event.data.reference_id) { activateUser(event.data.reference_id, event.data.id) } res.sendStatus(204) }) // 3. Success page polls a backend endpoint that the webhook populates app.get('/subscribed', (req, res) => { res.render('thank-you', { pollUrl: '/me/subscription-status' }) })
The success page never trusts the redirect itself — it polls a backend endpoint that checks the user's octany_subscription_id, populated by the webhook handler. This pattern survives closed tabs, async 3DS, and adblockers stripping query params.