# Repejo embedded checkout

> Put Repejo's checkout on your own page with one script tag and one custom
> element. Repejo renders the amount screen, the donor form, the payment
> (Swish, card, Apple Pay / Google Pay, Autogiro, …) and the thank-you
> screen; your page gets DOM events when the donor is done. No API token
> and no backend are needed. For your own amount buttons or donor form use
> Payment Sessions instead (see the next section).

- TypeScript / React types for every element: https://app.repejo.se/assets/repejo-checkout.d.ts
- Payment Sessions (server-side; own amount / donor UI): https://app.repejo.se/docs/api-payment-sessions.md
- Webhooks: https://app.repejo.se/docs/api-webhooks.md
- Human docs: https://app.repejo.se/docs/website-implementation and https://app.repejo.se/docs/web-component

## Embedded checkout or Payment Sessions?

Decide before writing front-end code; if the integrating organisation hasn't
said which they want, **ask** — the two need different code.

| You want | Build |
|----------|-------|
| Repejo's own amount screen and donor form on your page; no backend | **Embedded checkout** — this document |
| Fixed-amount one-time Swish buttons ("Swisha 100 kr") with no backend | `<repejo-donate-button amount="…">` — this document |
| The site's own button, link or menu item opening Repejo's modal, optionally with the amount and the reference preselected; no backend | `href="?rp_modal=…"` or `data-repejo-checkout="…"` — this document |
| Your own amount buttons or donor form, or the payment created from your server with per-donor `metadata` | **Payment Sessions** — `POST /api/v1/payment_session`, then redirect or `<repejo-checkout session-token="…">`; needs an organisation API token and an API-type checkout |
| Fundraising team competitions (`team`, `teams-picker`) | Embedded checkout only — not available on payment sessions |

The embedded element **cannot take an amount**: there is no amount attribute
and no amount query parameter. The site's own button can open the checkout
modal with an amount preselected (`data-repejo-amount`, below), but the donor
still sees Repejo's amount screen and can change it. Skipping that screen
always means Payment Sessions. Both integrations can live on the same site.

## Prerequisites

1. A **checkout** created in the Repejo back office (*Settings → Checkouts*).
   Everything the donor sees is configured there, not in markup: the
   periodicities (one-time, monthly, yearly — a checkout can offer two and
   the donor switches between them), the payment methods, predefined and
   minimum amounts, required donor fields, texts, colours, fonts, languages,
   the fundraising goal, and the campaign section (image + story + progress
   bar rendered next to the form when *Branding → Campaign → Show campaign
   section* is on).
2. The checkout's **short code** — the embed handle, e.g. `aunt123`. Copy it
   from the checkout's *Användning* (Usage) tab, which also shows the finished
   snippet. It is **not** the `chk_…` checkout id; that id is only used by
   the Payment Sessions API. If it is unclear which value you were given, ask.
3. The **host**: `https://test.repejo.se` (the test environment — mock Swish,
   Autogiro and BankID, so a donation completes end to end without real
   money) or `https://app.repejo.se` (production). A short code exists only
   on the host where its checkout was created, so the script's `data-host`
   and every element's `host` must name that environment. The examples below
   use `https://app.repejo.se`, the environment that served this document.

## Quickstart

```html
<script defer src="https://app.repejo.se/assets/checkout.js" data-host="https://app.repejo.se"></script>

<repejo-checkout short_code="aunt123" host="https://app.repejo.se"></repejo-checkout>

<script>
  window.addEventListener("repejo:completed", (e) => {
    // e.detail.type: "recurring" | "onetime" | "gift_card" | "lead" — show your
    // own thank-you, or:
    window.location.href = "/thank-you";
  });
</script>
```

Load the script once per page (in `<head>` or before `</body>`); it defines
every `repejo-*` element below. The snippet on the Usage tab also carries
`data-organisation-id="org_…"` — on `checkout.js` it only feeds the Meta
Pixel page view on script load, so keep it if you copied it and leave it
out otherwise. (Repejo's own hosted page loads a separate consent script
that reads it too; that script is not part of an embed.)

A checkout has no thank-you redirect setting: after a completed donation the
element shows its own confirmation screen (text from branding). To send the
donor somewhere else, act on `repejo:completed`.

## Elements

Every element takes `short_code` and `host`. `short_code` is the canonical
attribute name (it mirrors the Lit property); `<repejo-checkout>` accepts the
kebab-case `short-code` as an alias. The live widgets at the end of this
document use `short-code`.

### `<repejo-checkout>` — the checkout inline on your page

Renders whichever checkout the short code points to: recurring donation,
one-time donation, membership, gift card, petition / lead form, … The form
sits in a card whose background, border and shadow come from branding (see
Styling).

| Attribute | Type | Notes |
|-----------|------|-------|
| `short_code` | string | Required. Alias: `short-code`. |
| `host` | URL | The Repejo environment. Defaults to `https://app.repejo.se` — set it explicitly. |
| `team` | `ftm_…` | Attribute the donation to a fundraising team in a team competition — the embedded form of the hosted page's `?rp_team=`. An unknown or unlinked id is ignored and the plain checkout renders. |
| `teams-picker` | boolean | Show a "choose your team" screen first, then the checkout. Skipped when `team` (or `?rp_team=`) is already known. Hosted-page equivalent: `?teams=picker`. |
| `metadata-<key>` | string | One attribute per public custom attribute, e.g. `metadata-member_id="42"`. Only keys defined as **public** under *Settings → Custom attributes* are accepted (`a-z`, `0-9`, `_`); other keys are ignored. Stored on the donor's session and echoed as `metadata` on the payer / subscription / transaction and on their webhooks. |
| `payer-name`, `payer-email`, `payer-phone-number` | string | Prefill the donor form. Validated per field server-side; a malformed value is dropped silently instead of blocking the donation. |
| `user-has-consented-to-marketing-cookies` | boolean | Set it when your consent tool reports marketing consent, so Meta Pixel / dataLayer events may fire. With Cookiebot on the page the consent is read automatically. |
| `live-product-id` | `prod_…` | Preselect a live product (the donor arrived via `<repejo-live-products>`); pins a summary card of it above the form. |
| `session-token`, `bare` | — | **Payment Sessions only**: a server-created session, and the chrome-less rendering it allows. Not for a plain embed — `bare` is ignored on every other checkout type. |

The checkout opens in the language of your page: the `lang` attribute on
`<html>`, matched on the language alone (`en-US` opens English), when the
checkout has that language enabled. Otherwise it opens in the checkout's
default language. The labels of `<repejo-checkout-button>` and
`<repejo-sticky-checkout>` follow the same rule. There is no language
attribute on the element; the donor can still switch with the checkout's
language selector.

Create it from JavaScript by setting attributes or properties, before or
after `appendChild` — the element fetches the checkout once `short_code` is
set:

```js
const el = document.createElement("repejo-checkout");
el.host = "https://app.repejo.se";
el.short_code = "aunt123";
el.setAttribute("metadata-member_id", "42");
document.querySelector("#donate").appendChild(el);
```

Older snippets use `<repejo-onetime-checkout>` for a one-time checkout. It
takes only `short_code` and `host` — `metadata-*`, `team`, `teams-picker`
and the `payer-*` prefill are silently ignored on it — so use
`<repejo-checkout>`, which renders the same form and supports every
attribute above.

### `<repejo-checkout-button>` — a button that opens the checkout in a modal

```html
<repejo-checkout-button short_code="aunt123" host="https://app.repejo.se"></repejo-checkout-button>
```

The label comes from the checkout's branding (*checkout button text*).
Attributes: `short_code`, `host`, `size` (`default` or `large`), `team`,
`teams-picker`, `metadata-<key>`, `payer-name` / `payer-email` /
`payer-phone-number`, `user-has-consented-to-marketing-cookies` — same
meaning as above; prefill and metadata are forwarded to the modal's
checkout. While the modal is open the page URL carries
`?rp_modal=<short_code>`, which is what reopens it when the donor returns
from the Swish app — leave that parameter alone in your router.

### `<repejo-sticky-checkout>` — an always-visible side tab that opens the modal

```html
<repejo-sticky-checkout short_code="aunt123" host="https://app.repejo.se"></repejo-sticky-checkout>
```

Place it once in the site layout; it renders a tab on the left edge of the
viewport and opens the same modal as the button. Same attributes as the
button except `size`, plus `side`: `side="right"` puts the tab on the right
edge, `side="left"` or no `side` on the left. Any other value leaves the tab
on the left edge.

### `<repejo-donate-button>` — one-click Swish with a preset amount

```html
<repejo-donate-button short_code="swish1" amount="100" host="https://app.repejo.se"></repejo-donate-button>
```

For a **one-time** checkout. `amount` is in the checkout's currency (`100` =
100 SEK — not öre). The direct Swish flow needs Swish to be the checkout's
**only one-time payment method**: the button then reads "Swisha 100 kr" and
a click starts a Swish donation of that amount — on a phone the Swish app
opens with the amount filled in, on desktop the donor enters a phone number
first. With more than one one-time method enabled the button reads
"Donera 100 kr" and the modal opens on the payment-method picker instead.
Every donor field the checkout marks as required is still asked for in the
modal, so for a true one-click experience create a dedicated one-time
checkout with Swish alone and only the phone number required. Several
buttons with different amounts give you an amount picker without a
backend. Attributes: `short_code`, `host`, `amount` (required), `team` (no
team picker here), `payer-name` / `payer-email` / `payer-phone-number`,
`user-has-consented-to-marketing-cookies`.

### Gift shop (Gåvoshop) — product list and cart

A checkout of product type Gåvoshop (physical products) sells products with
a cart. Three ways to embed it:

```html
<repejo-physical-product-list short_code="shop1" host="https://app.repejo.se"></repejo-physical-product-list>
<repejo-physical-product-cart-button short_code="shop1" host="https://app.repejo.se" position="bottom-left"></repejo-physical-product-cart-button>
<repejo-checkout short_code="shop1" host="https://app.repejo.se"></repejo-checkout>
```

- `<repejo-physical-product-list>` renders every active product as a card
  grid that fills the container. A card's buy button adds the product to
  the cart; a click on the card opens a product modal with all images, the
  full description and the variant picker (size, colour or a custom
  option). The list renders its own floating cart button: do not add a
  second one on the same page. The cart opens as a right-side drawer that
  holds the whole purchase: cart, donor details, payment, confirmation.
- `<repejo-physical-product-cart-button>` is the floating round button on
  its own, for the site's other pages. It shows only while the cart has
  items, and opens the same drawer. `position` is `bottom-right`
  (default) or `bottom-left`.
- `<repejo-checkout>` with a Gåvoshop short code renders the whole shop
  inline: products on the left, cart and payment on the right.

The cart lives in `localStorage` per short code, so it survives reloads and
is shared by the list and the cart button on the same site. It is cleared
after a completed purchase. The order total is the products, plus the
checkout's fixed shipping fee, plus an optional extra gift the buyer adds in
the cart (when enabled in branding). Name and delivery address are always
required; payment is one-time only (Swish, card, Apple Pay, Google Pay).

### The site's own button or link — `?rp_modal=` and `data-repejo-*`

With the script tag on the page, the site's own markup opens the same modal
as `<repejo-checkout-button>` — no JavaScript to write. Use it wherever you
can only set a link address or attributes on an element.

```html
<!-- A link to the same page: works in any field that takes a link address -->
<a href="?rp_modal=aunt123">Donate</a>

<!-- Any element with data-repejo-checkout; amount and reference optional -->
<a href="https://app.repejo.se/s/aunt123"
   data-repejo-checkout="aunt123"
   data-repejo-amount="5000"
   data-repejo-reference="malmo">Give 5 000 kr to Malmö</a>
```

- **`?rp_modal=<short_code>` links.** A click on a link to the same page
  (same origin and path) with `rp_modal` in its query opens the modal without
  a reload. Cmd/ctrl/shift/alt clicks and links with a `target` other than
  `_self` follow the link as usual. A link to another page — or from a
  newsletter or an ad — opens the modal when that page has loaded. On load
  the parameter is ignored when the URL also carries `rp_token` or `rp_slug`
  (the donor is returning from a payment), and when the page already shows
  that checkout itself (an inline `<repejo-checkout>`, a checkout button,
  company packages, live products, a shop) — and on pages with a P2P list,
  top list or campaign element, which open their own checkouts.
- **`data-repejo-checkout="<short_code>"`** on any element: a click on it,
  or on anything inside it, opens the modal — also on a link with
  `target="_blank"`, whose `href` is then only the fallback for a page where
  the script has not loaded (point it at the hosted page as above).
- **`data-repejo-amount`** preselects an amount in the checkout's currency
  (`5000` = 5 000 SEK — not öre; `"5 000 kr"` is read the same way). One of
  the checkout's amount buttons is selected; any other amount goes into the
  other-amount field, unless the checkout hides that field — then the amount
  is ignored. An amount below the checkout's minimum is preselected, and the
  donor gets the usual minimum-amount message on continue.
- **`data-repejo-reference`** preselects one of the checkout's references
  ("Flera referenser") by its **value** — the back office field *Referens*
  (*Reference* in English), not *Titel för ändamål* (*Title for purpose*).
- The donor can change both before paying. The attributes are read at the
  click, so the page's own script may update them (e.g. from a package or
  place picker on the page).
- Preselection applies to gift checkouts (product type Gåva, also P2P and
  greeting gifts). A preselection on another checkout type, an unknown
  reference, an amount the checkout cannot take and an unknown short code
  are reported to Repejo as frontend warnings and ignored; the donor sees no
  error (an unknown short code opens nothing).
- One modal at a time. While it is open the page URL carries
  `?rp_modal=<short_code>`, as with the checkout button.
- Unlike the checkout button, a modal opened this way is **not** reopened
  when the donor returns from the Swish app on a phone (the return URL
  carries `rp_token`); the donation is still registered, but the donor
  lands on the page without the thank-you screen.
- Use `<a href>` or `<button>` so the trigger also works from the keyboard,
  and give it `cursor: pointer` if the site's CSS does not. While the
  checkout loads the element carries `aria-busy="true"` and a progress
  cursor.

### The hosted campaign page — no embed at all

Every checkout also has a campaign page on Repejo's host, `https://app.repejo.se/s/aunt123`
(share the link or the QR code from the Usage tab). The organisation builds
it from blocks under the checkout's Campaign page (Kampanjsida) tab in the back office; a
page that was never edited shows a title, a text and the checkout.
`?rp_team=ftm_…` attributes donations to a team; `?teams=picker` opens the
team picker first.

## Events

The elements dispatch DOM `CustomEvent`s that **bubble up to `window`**.
`<repejo-checkout>` dispatches them itself, so listening on the element
works too; the modal embeds (`<repejo-checkout-button>`,
`<repejo-sticky-checkout>`, `<repejo-donate-button>`) render the checkout
in a portal at the end of `<body>`, so listen on `window` or `document`,
not on the button.

| Event | When | `event.detail` |
|-------|------|----------------|
| `repejo:first-interaction` | the donor first interacts with the form | — |
| `repejo:registered` | the donor left the amount / contact step; on a petition or lead checkout this is the sign-up itself | — |
| `repejo:completed` | recurring mandate signed, one-time payment captured, gift card bought, or lead registered | see below |
| `repejo:cancelled` | **Payment Sessions only** — the donor pressed the cancel button of an embedded session | `{ session_token }` |

A plain embed never fires `repejo:cancelled`: there is no cancel step, the
donor just leaves the page or closes the modal.

```ts
type RepejoCompletedEventDetail =
  | {
      checkout_short_code: string;
      type: "recurring" | "onetime" | "gift_card" | "payment_method"; // payment_method: a mandate set up by a request_type "payment_method" Payment Session
      payer: { name: string | null; phone_number: string | null; email: string | null };
      metadata: Record<string, string>; // the metadata-* attributes, echoed back
    }
  | { type: "lead" } // petition / lead sign-up: no payer, no metadata
  | { type: "onetime"; source: "post_signature_donation" }; // a gift added after signing a petition
```

Only `repejo:completed` is part of the contract; the other events are
informational. The events are a UX convenience (thank-you page, analytics)
— for bookkeeping use webhooks (below). The declaration file at
`https://app.repejo.se/assets/repejo-checkout.d.ts` types the events on `window` and on
the elements, the element properties, and JSX for React.

## Styling

Typography is host-overridable with CSS custom properties on the element:

```css
repejo-checkout {
  --font-sans: Roboto, sans-serif;
  --font-weight-xl: 400;
  --font-weight-lg: 500;
  --font-weight-base: 400;
}
```

Available: `--font-sans` (base font) and, per size, `--text-xl` /
`--font-family-xl` / `--font-weight-xl` (large headings, 24px, weight 600),
`--text-lg` / `--font-family-lg` / `--font-weight-lg` (subheadings, 18px,
500), `--text-base` / `--font-family-base` / `--font-weight-base` (body,
16px, 400), `--text-sm` / `--font-family-sm` / `--font-weight-sm` (small,
14px, 600) and `--text-xs` (12px). A web font must be loaded by your page
(`@font-face` or a font link) before the element renders.

`<repejo-checkout-button>` takes its button text font from `--font-sans`
too — set it on the button element. Only the font family changes, not the
weight or size of the button text:

```css
repejo-checkout-button {
  --font-sans: Roboto, sans-serif;
}
```

The card itself — background, border, shadow, width — every colour, and the
input corner radius and text inset come from the checkout's **branding** in
the back office. They are **not** overridable from host-page CSS variables:
branding writes them onto the component, so a page-level
`--color-checkout-background` has no effect. To blend the checkout into a
coloured page, set the checkout background to the page colour (or
transparent), border width 0 and "hide box shadow" in branding. Pill-shaped
inputs need a larger text inset so the text clears the curve. `bare` removes
the card entirely, but only on Payment Sessions.

## Reconciling donations

Every completed donation lands in the back office (the donor under
*Givare*, the payment under the checkout's transactions). For your own
systems, register a webhook endpoint in the back office and subscribe to:

- `payer.created` / `payer.updated` — the donor record.
- `subscription.created` / `subscription.updated` — a recurring mandate signed on a recurring checkout.
- `receivable.created` / `transaction.created` (and their `.updated`) — a payment: a one-time gift, or one month of a recurring gift.

Your public `metadata-*` values arrive as `data.metadata` on each of them.
`payment_session.*` events fire **only** for Payment Sessions — a plain embed
never emits them. Payload shapes, HMAC-SHA256 signature verification and the
retry / idempotency contract: https://app.repejo.se/docs/api-webhooks.md

## Analytics

With *Settings → Integrations → Google Tag Manager* on, the payment
checkouts (recurring, one-time, membership, gift card) push GA4 funnel
events, a server-built `purchase` and `repejo_click` element interactions to
the page's `dataLayer`, gated on marketing consent (Cookiebot or the consent
attribute above); petition / lead checkouts push none of these:
https://app.repejo.se/docs/gtm-datalayer.md

## Live widgets

Standalone elements that read the same checkout and update live over a
websocket — for a campaign page next to (or instead of) the form. They take
`short-code` (kebab-case), `host` and an optional `locale`, style themselves
through a few attribute shortcuts (`background-color`, `text-color`, …) plus
per-widget CSS custom properties, and need `checkout.js` on the page:

| Element | Shows |
|---------|-------|
| `<repejo-progress-bar>` | raised vs goal (or signatures vs signature goal) in the checkout's branding |
| `<repejo-donation-goal>` | goal progress bar with sub-goal ticks; `team-id="ftm_…"` tracks one team |
| `<repejo-counter>` | a live count of gifts (signatures on a petition) plus a start value, between optional texts; no goal needed |
| `<repejo-donation-list>` | the three most recent donations, rolling |
| `<repejo-donation-ticker>` | a scrolling news-ticker of recent donations |
| `<repejo-leaderboard>` | top donors or teams (`mode` = `teams`, `onetime` or `recurring`; `limit`) |
| `<repejo-top-donation>` | the single biggest one-time gift |
| `<repejo-donation-card>` | stream overlay (OBS / Twitch) popping a card per donation |
| `<repejo-matching-donation-card>` / `-row` | an active sponsor matching campaign |
| `<repejo-live-products>` | a mini-shop of preset gift products that opens the checkout |

The full attribute and CSS-variable reference for each widget is in
`https://app.repejo.se/llms.txt` (section "Embeddable elements").
