> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orriven.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Storefront API

> Build your event site with zero backend: a publishable key your frontend can hold, attendee sign-in with platform-sent OTP emails, catalog, checkout and a buyer account area — all called straight from the browser.

The Storefront API is Orriven acting as your entire backend. Where the [Developer API](/en/developers/overview) assumes *buyer → your frontend → your backend (holding the secret key) → Orriven*, the Storefront API removes the middle hop: your frontend calls Orriven directly with a **publishable key** that is safe to ship in browser code. Sign-in, session handling, OTP emails, catalog rules, capacity, payment and fulfilment are all the platform's job — you build only the pages.

<Note>
  A publishable key serves **one portal** — the same identity realm and the same catalog as that portal's hosted site. A buyer who signs in on your storefront and on the hosted portal is the **same account**, with the same registrations and orders.
</Note>

## Two keys, two shapes

|                                     | Secret key (`osk_…`)                | Publishable key (`opk_…`)             |
| ----------------------------------- | ----------------------------------- | ------------------------------------- |
| Lives in                            | Your server only                    | Your frontend — public by design      |
| Scope                               | The whole Business Unit, read/write | One portal's public storefront        |
| Surface                             | `/v1`                               | `/storefront/v1`                      |
| Sees counts, drafts, attendee lists | Yes                                 | Never                                 |
| If leaked                           | Rotate immediately                  | Equivalent to knowing your site's URL |

The two are mutually exclusive on the wire: a publishable key on `/v1` answers the same `401` as an invalid key, and a secret key on `/storefront/v1` does too.

A leaked publishable key can read the published catalog, place rate-limited orders, and trigger rate-limited OTP emails — nothing more. It cannot read anyone's account (that takes a proven inbox), see numeric availability, or touch the Developer API. Revoking it works like revoking any key: immediately.

## Setup

<Steps>
  <Step title="Create a publishable key">
    On the **API keys** page choose **Publishable key**, pick the portal it serves (your Business Unit's own portal by default) and list the **origins** your site runs on — `https://tickets.your-site.com`, one per line (`http://localhost:…` is fine for development). The key appears in full in the list, any time — there is no one-time secret ceremony, because the key is public.
  </Step>

  <Step title="Publish the portal">
    The key serves traffic only while its portal is **published** — that is the one go-live switch, even if you never use the hosted portal pages. An unpublished portal answers `412` with an explanation.
  </Step>

  <Step title="Call from the browser">
    ```js theme={null}
    const BASE = "https://openapi.orriven.com/storefront/v1";
    const KEY = "opk_…";   // safe to ship

    const res = await fetch(`${BASE}/events`, {
      headers: { Authorization: `Bearer ${KEY}` },
    });
    ```

    CORS is open to exactly the origins on the key. Requests from any other origin are refused — that check contains someone embedding your key in *their* site; it is not what protects buyer data (sessions are).
  </Step>
</Steps>

## Attendee sign-in — OTP by the platform

Your site gets accounts without owning any auth. The platform emails the code and issues the session:

```js theme={null}
// 1. Ask for a code — Orriven sends the email.
await post(`${BASE}/auth/otp`, { email });

// 2. Exchange the code for a session token.
const { token, expiresAt, attendee } =
  await post(`${BASE}/auth/otp/verify`, { email, code });

// 3. Send it on account requests — a header, never a cookie.
const me = await fetch(`${BASE}/account/me`, {
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Orriven-Attendee-Token": token,
  },
});
```

* The answer to `/auth/otp` is identical for known and unknown addresses — an account exists the moment its inbox is proven, so there is nothing to enumerate. Codes last 10 minutes, allow 5 attempts, and re-sending is throttled to once a minute per address.
* The token (`oat_…`) is the person's session on this portal: 30 days on a Business Unit portal, 12 hours on a standalone event's. Keep it in memory or storage your frontend controls; `POST /auth/logout` revokes it server-side.
* Wrong, expired, spent and over-guessed codes are all the same refusal — which one it was is not the guesser's information.

## The account area

Behind the token, four reads power a complete "my tickets" page:

| Endpoint                                | Answers                                                                                                                                                                                                                                                                                                    |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /account/me`                       | Who is signed in                                                                                                                                                                                                                                                                                           |
| `GET /account/registrations`            | Their registrations, each with the event head and — on a confirmed seat of an on-site event — their **entry-pass code** to render as a QR. When the person staffs an exhibiting company, `exhibitor` names it — company, status, booth allocations — so an exhibitor-facing page knows whose booth to load |
| `GET /account/registrations/{id}/perks` | What that admission includes (ticket ∪ code ∪ individual grants), names and descriptions                                                                                                                                                                                                                   |
| `GET /account/orders`                   | Their orders on this portal, by the signed-in address                                                                                                                                                                                                                                                      |

## Catalog and checkout

The catalog is the hosted portal's, shape for shape: `GET /events` lists the portal's published events, `GET /events/{publicId}` answers everything a sales page needs — ticket types (`soldOut` is a **boolean**, never a count), add-ons, bundles, the open agenda, speakers, published registration forms. Hidden ticket types are absent; `GET /events/{publicId}/redemption-codes/lookup?code=` is the one road to them, and every invalid code answers the same `404`.

The **sale window is public information**: on-sale types carry `opensAt`/`closesAt`, and `upcomingTicketTypes` lists types announced but not yet on sale — name, price, perks, and the `opensAt` instant your countdown ticks toward. Announcement is all it is: placing an order for an upcoming type still refuses until the window opens.

Checkout is the same three steps as everywhere on the platform — place, confirm, poll — now callable from the page itself:

```js theme={null}
// Place: holds its seats for 15 minutes. Signed in, email may be omitted.
const { order } = await post(`${BASE}/events/${publicId}/orders`,
  { ticketTypeId }, token);

// Confirm: a free order settles right here…
const done = await post(
  `${BASE}/events/${publicId}/orders/${order.orderNumber}/confirm`,
  { successUrl: location.origin + "/thanks",
    cancelUrl: location.origin + "/checkout" });

// …a paid one answers { status: "awaiting_payment", paymentUrl } —
// send the buyer there, then poll GET /orders/{orderNumber} to "paid".
```

Anonymous checkout stays first-class: an `email` in the body is all it takes, exactly like the hosted portal. A buyer already registered is acknowledged with `alreadyRegistered: true`, never sold a second seat. Return URLs must be absolute `https` (plain `http` allowed on localhost).

## Interactive reference

The surface describes itself: [`$BASE/storefront/openapi.json`](https://openapi.orriven.com/storefront/openapi.json) is the full OpenAPI 3.1 document and `$BASE/storefront/docs` the try-it reference. Every endpoint also has a page in the **Storefront endpoints** group in this site's sidebar. [Date versioning](/en/developers/versioning) works exactly as on the Developer API: the key pins its creation-time version, `Orriven-Version` overrides per request.

## Rules to know

* **Never numbers.** This surface answers `soldOut: true/false` and nothing else about capacity. Live counts belong to the Developer API's `availability`, behind the secret key.
* **404, not 403.** A draft event, another portal's event and a nonexistent id are indistinguishable from this side.
* **Rate limits protect the mailer.** OTP requests are limited per key and per visitor IP on top of the per-address throttle; order-number reads carry the hosted portal's own per-IP wall.
* **Storefront traffic is request-logged, not audited.** Calls appear in the console's developer log for debugging, but OTP requests and checkouts are the *buyer's* actions and do not enter the organization audit log. Automations still fire on fulfilment exactly as always.

## Related

<CardGroup cols={2}>
  <Card title="API keys" icon="key" href="/en/developers/api-keys">
    Creating publishable keys and managing their origins.
  </Card>

  <Card title="Headless checkout (secret key)" icon="cart-shopping" href="/en/developers/checkout">
    The server-side flow — for when you do run a backend.
  </Card>
</CardGroup>
