Octany. for/devs view as .md
§ 02 — Checkout

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.

If you only read one section Build a URL on your server, redirect the user, then listen for the 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.

Domain Production: 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.

ParamPurpose
successUrlWhere Octany redirects after a successful payment. Must be HTTPS.
referenceIdUse 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.
referenceNameHuman-readable label that shows up in the admin UI alongside the resource.
emailPre-fills the customer email field.
firstName, lastNamePre-fills name fields.
companyNamePre-fills business-customer fields.
roleSingle role to attach to the user (CRM segmentation).
rolesComma-separated roles, e.g. roles=member,donor.
amountInteger in öre/cents. Overrides the product's price for this session — for "name your price" / top-up flows.
localesv, 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:

Why this matters The user might close the tab between payment and redirect. 3DS can complete asynchronously. Adblockers occasionally strip query params. The webhook always arrives. Trust the webhook.

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:

  1. 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.
  2. 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 like myapp://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:

EventWhenevent.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.