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-id}/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}/{id} 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. |
| methods | Comma-separated allow-list of payment methods to show, e.g. methods=stripe,swish. Useful when you want a Swish-first or card-only flow. |
| onlyMethod | Backwards-compatible single-method alias for methods, e.g. onlyMethod=swish. Prefer methods for new integrations. |
| quantity | true/false. Set quantity=false to hide quantity controls and prevent the buyer from changing item count in checkout. |
| locked | Backwards-compatible force-lock flag. locked=true locks quantity even if quantity=true is present. Prefer quantity=false for new integrations. |
| 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>
Restrict payment methods or lock quantity
If you want the hosted checkout to show only specific payment methods, pass methods on the URL. Octany matches values against the methods available for that product and account, ignoring anything inactive or unsupported.
https://checkout.octany.com/checkout/{account}/product/{product-id}?methods=stripe,swish&quantity=false
Use quantity=false to prevent quantity changes in checkout. Older integrations may still send locked=true or onlyMethod=swish; both continue to work, but new integrations should use methods and quantity.
§ 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-id}', locale: 'en', // optional; defaults to account locale methods: ['stripe', 'swish'], quantity: false, } </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.
window.OctanyConfig.methods is the embed equivalent of the hosted checkout's methods query parameter: it limits which payment methods the checkout shows. Set quantity: false to lock line-item quantities for checkout sessions launched from the embed.
§ 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.