# Frontend integration

This guide covers browser-side Octany integrations: **Fundraising**,
**Checkout**, and **Classic widget**.

If you're building server-side flows, start with
[`integration.md`](integration.md) instead.

## Public surfaces

| Surface | When to use |
| --- | --- |
| **Fundraising** | Donation, 1-click Swish, and Greeting embeds for campaign pages, editorial pages, and seasonal giving. |
| **Checkout** | Hosted checkout for subscriptions, one-off purchases, carts, paywalls, and app-driven payment flows. |
| **Classic widget** | Existing integrations that already use the original `<octany-widget>` custom element. |

Use **Fundraising** for donation embeds in public copy. "Widget" is
reserved for the classic checkout integration and for literal code
identifiers such as `widget={widget-id}`.

## 1. Fundraising

Fundraising embeds are self-contained donation experiences. They are
separate from Checkout.

| Experience | Use it for | Loader path | Container class |
| --- | --- | --- | --- |
| **Donation** | Modern donation flows for one-off and recurring gifts. | `donate` | `.octany-donate` |
| **1-click Swish** | Fast Swish-first giving where the donor chooses an amount and continues in Swish. | `swish-v1` | `.octany-swish-widget` |
| **Greeting** | Donation with a greeting card, tribute, or seasonal message, such as Mother's Day. | `greeting-v1` | `.octany-greeting-widget` |

### Embedding pattern

```html
<div class="octany-swish-widget"></div>
<script
  type="module"
  src="https://give.octany.com/swish-v1/loader.js?api=https://api.octany.com/widgets&widget={widget-id}&lang=sv"
></script>
```

The `api`, `widget`, and optional `lang` query parameters identify the
embed configuration and language.

### Theming

Theming uses CSS custom properties on the host page:

```css
:root {
  --octany-primary: #c8102e;
  --octany-radius: 8px;
}
```

Your Octany contact can provide the available variables for the
fundraising experience you are using.

## 2. Checkout URL

The simplest Checkout integration. You construct a URL server-side and
link or redirect to it. Octany handles cart, customer, payment method,
SCA, and the success page.

```text
https://checkout.octany.com/checkout/{account-uuid}/product/{product-id}?email=...&referenceId=...&successUrl=...
```

When the visitor opens the URL, Octany creates a checkout session, adds
the product to the cart, and continues the payment flow on
`checkout.octany.com`.

### Query parameters

| Param | Purpose |
| --- | --- |
| `successUrl` | Where Octany redirects after a successful payment. Must be HTTPS. |
| `referenceId` | Your ID for the visitor, member, donor, or order. Surfaces on the resulting `Subscription`/`Order` and on every webhook for that resource. **Use this to correlate.** |
| `referenceName` | Human-readable label shown in Octany admin views. |
| `email` | 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 for segmentation. |
| `roles` | Comma-separated roles, e.g. `roles=member,donor`. |
| `amount` | Integer in **öre/cents**. Overrides the product's price for this session, useful for "name your price" or top-up flows. |
| `locale` | `sv`, `en`, etc. Defaults to the account locale. |
| `utm_source` / `utm_medium` / `utm_campaign` / `utm_content` / `utm_term` | UTM parameters captured into the cart so they appear on the order/subscription. |

Anything you don't pass is left to the visitor to fill in. The product
itself is the only required input.

### Minimal example

```html
<a href="https://checkout.octany.com/checkout/{account}/product/42?referenceId=member_99&email=anna@example.com&successUrl=https://app.example.com/welcome">
  Subscribe - 99 SEK / month
</a>
```

### Wiring success to your app

Don't trust query params on `successUrl` for state changes. Octany's
hosted checkout redirects to your `successUrl` after the visitor
acknowledges payment, but **the source of truth for "did this actually
get paid" is the webhook** (`order.paid`, `subscription.created`).

The robust flow:

1. Visitor lands on your `successUrl`.
2. Show a "Thanks, finishing up..." page.
3. Wait for the corresponding webhook on your backend and correlate via `reference_id`.
4. Activate the user, unlock the feature, or mark the purchase as complete.

This handles closed tabs, asynchronous 3DS, and adblockers that strip
query params.

### 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 viable patterns:

1. **Poll your own backend** on the success page. Works, but races the
   webhook — if your 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`).

```js
// 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. This pattern works even if the webhook never arrives.

## 3. Embeddable cart

For multi-product carts on a content site, use the embeddable cart. It
loads a floating cart button, a slide-out cart container, and the
`window.Octany` JavaScript API.

### Install

```html
<script>
  window.OctanyConfig = {
    id: '{account-uuid}',
    locale: 'en',
  }
</script>

<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` and
`app.css` from `embed.octany.com`, so your page can stay on the stable
loader URL while Octany ships new versions.

### `window.Octany` JavaScript API

Once the loader has booted, `window.Octany` is available. If you need to
gate calls, listen for `octany-cart-ready` on `document`.

```js
Octany.set({
  referenceId: 'member_42',
  referenceName: 'Anna Persson',
  email: 'anna@example.com',
})

Octany.checkout(productId, {
  successUrl: 'https://app.example.com/welcome',
  amount: 19900,
})

Octany.cart.add(productId)
Octany.cart.open()
Octany.cart.close()
Octany.cart.hide()
Octany.cart.checkout()
Octany.cart.requestCheckoutUrl()
```

### DOM events

The cart dispatches custom events on `document`:

| Event | When | `event.detail` |
| --- | --- | --- |
| `octany-cart-updated` | Items added, removed, or quantity changed. | `{ cart: { items: [...], total, ... } }` |

Use these to update your own page chrome, such as a cart count badge,
without reading the cart iframe directly.

### UTM auto-capture

The embed reads UTM parameters from the host page's URL on load and
forwards them through `Octany.set(...)` so they land on the resulting
order's `utm_parameters`.

### 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**: `update_card_url`, `invoices_url`, `cancel_url`. To send a
logged-in user to "manage subscription", fetch their subscription
server-side (`GET /subscriptions?filter[reference_id]=…`) and redirect
to the URL you want. There is no "give me a fresh portal URL for this
customer" endpoint — the URLs come back inline on the subscription.

## End-to-end example: paywall with hosted checkout + webhook

```js
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}`)
})

app.post('/octany/webhook', verifyOctanySignature, (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)
})

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 activation performed by the webhook handler.
