> ## 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

> 0 后端搭建你的活动网站:前端可持有的公开密钥、平台代发 OTP 邮件的参会者登录、目录、购买流与买家用户中心——全部从浏览器直连。

Storefront API 就是 Orriven 替你当整个后端。[开放 API](/zh/developers/overview) 的形态是*买家 → 你的前端 → 你的后端(持 Secret 密钥)→ Orriven*;Storefront API 把中间那一跳去掉了:你的前端拿一把\*\*可以直接写进浏览器代码的公开密钥(Publishable Key)\*\*直连 Orriven。登录、会话、OTP 邮件、目录规则、容量、支付、履约全都是平台的事——你只需要写页面。

<Note>
  一把公开密钥服务**一个门户**——与该门户托管站点是同一个身份域、同一份目录。买家在你的网站和托管门户上登录的是**同一个账号**,报名与订单完全一致。
</Note>

## 两种密钥,两种形态

|                 | Secret 密钥(`osk_…`) | Publishable 公开密钥(`opk_…`) |
| --------------- | ------------------ | ------------------------- |
| 放在哪             | 只能在你的服务端           | 你的前端——公开是它的设计             |
| 作用域             | 整个业务单元,可读写         | 一个门户的公开 storefront        |
| 调用面             | `/v1`              | `/storefront/v1`          |
| 能看到数字库存、草稿、参会名单 | 能                  | 永远不能                      |
| 泄露后果            | 立即轮换               | 等价于别人知道了你网站的网址            |

两者在线路上互斥:公开密钥打 `/v1` 得到与无效密钥完全相同的 `401`,Secret 密钥打 `/storefront/v1` 同样如此。

一把泄露的公开密钥能做的只有:读已发布的目录、受限流地下单、受限流地触发 OTP 邮件——仅此而已。它读不到任何人的账户(那需要证明收件箱)、看不到数字库存、上不了开放 API。吊销它与吊销任何密钥一样:立即生效。

## 接入步骤

<Steps>
  <Step title="创建公开密钥">
    在「API 密钥」页选择**Publishable 公开密钥**,选定它服务的门户(默认是业务单元自己的门户),并填写你网站运行的 **Origin** —— `https://tickets.your-site.com`,每行一条(开发环境可以用 `http://localhost:…`)。密钥全文随时在列表中可见、可复制——没有"只显示一次"的仪式,因为它本来就是公开的。
  </Step>

  <Step title="发布门户">
    密钥只在其门户处于**已发布**状态时提供服务——即使你完全不用托管门户页面,这也是唯一的"上线"开关。未发布的门户会回答 `412` 并说明原因。
  </Step>

  <Step title="从浏览器调用">
    ```js theme={null}
    const BASE = "https://openapi.orriven.com/storefront/v1";
    const KEY = "opk_…";   // 可以放心随页面分发

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

    CORS 恰好向密钥上登记的 Origin 开放。其他 Origin 的请求会被拒绝——这道校验防的是别人把你的密钥嵌进*他的*网站,真正保护买家数据的是会话,不是它。
  </Step>
</Steps>

## 参会者登录——平台代发 OTP

你的网站拥有账号体系,却不用自己做任何认证。验证码邮件由平台发出,会话由平台签发:

```js theme={null}
// 1. 请求验证码——邮件由 Orriven 发出。
await post(`${BASE}/auth/otp`, { email });

// 2. 用验证码换会话令牌。
const { token, expiresAt, attendee } =
  await post(`${BASE}/auth/otp/verify`, { email, code });

// 3. 账户请求带上它——走请求头,永远不是 cookie。
const me = await fetch(`${BASE}/account/me`, {
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Orriven-Attendee-Token": token,
  },
});
```

* `/auth/otp` 对已知与未知地址的回答完全相同——收件箱被证明的那一刻账号即存在,所以没有什么可枚举的。验证码 10 分钟有效、允许 5 次尝试,同一地址一分钟内只能重发一次。
* 令牌(`oat_…`)是此人在这个门户上的会话:业务单元门户 30 天,独立活动门户 12 小时。放在你前端可控的内存或存储里;`POST /auth/logout` 会在服务端吊销它。
* 错误、过期、已用、猜多了的验证码得到同一句拒绝——具体是哪一种,不是猜码者该知道的信息。

## 用户中心

令牌之后,四个只读端点就能撑起完整的「我的票」页面:

| 端点                                      | 回答                                                                                                       |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `GET /account/me`                       | 谁在登录                                                                                                     |
| `GET /account/registrations`            | 本人的报名,每条带活动摘要;开启了现场签到的活动、已确认的座位,还带**入场码**(渲染成二维码)。此人在替展商站台时,`exhibitor` 会给出公司名、状态与摊位分配——展商侧页面据此加载自家摊位的内容 |
| `GET /account/registrations/{id}/perks` | 这次入场包含什么(票种 ∪ 兑换码 ∪ 个人授予),只有名字与描述                                                                        |
| `GET /account/orders`                   | 本人在此门户上的订单,按登录邮箱归属                                                                                       |

## 目录与购买流

目录与托管门户逐字段一致:`GET /events` 列出门户的已发布活动,`GET /events/{publicId}` 回答销售页需要的一切——票种(`soldOut` 是**布尔值**,永远不是数字)、加购、捆绑、开放议程、讲者、已发布的报名表单。隐藏票种不在目录里;`GET /events/{publicId}/redemption-codes/lookup?code=` 是通往它们的唯一道路,任何无效的码都得到同一个 `404`。

**售卖窗口是公开信息**:在售票种带 `opensAt`/`closesAt`;`upcomingTicketTypes` 单列「已公布、未开售」的票种——名称、价格、权益,以及你的倒计时要数到的那个 `opensAt` 时刻。公布仅仅是公布:倒计时归零之前,对未开售票种下单仍会被拒绝。

购买流与平台各处相同的三步——下单、确认、轮询——现在可以从页面本身发起:

```js theme={null}
// 下单:占位 15 分钟。已登录时 email 可省略。
const { order } = await post(`${BASE}/events/${publicId}/orders`,
  { ticketTypeId }, token);

// 确认:免费单当场结清……
const done = await post(
  `${BASE}/events/${publicId}/orders/${order.orderNumber}/confirm`,
  { successUrl: location.origin + "/thanks",
    cancelUrl: location.origin + "/checkout" });

// ……付费单返回 { status: "awaiting_payment", paymentUrl } ——
// 把买家送过去,然后轮询 GET /orders/{orderNumber} 直到 "paid"。
```

匿名结账仍是一等公民:body 里给一个 `email` 就够,与托管门户完全一致。已报名的买家得到 `alreadyRegistered: true` 的确认,绝不会被卖第二个座位。回跳地址必须是绝对 `https` 地址(localhost 允许 `http`)。

## 交互式参考

这个调用面会自我描述:[`$BASE/storefront/openapi.json`](https://openapi.orriven.com/storefront/openapi.json) 是完整的 OpenAPI 3.1 文档,`$BASE/storefront/docs` 是可试调的参考页。每个端点在本站侧边栏的「Storefront 接口」组里也各有一页。[日期版本](/zh/developers/versioning)与开放 API 完全一致:密钥钉住创建时的版本,`Orriven-Version` 头可按请求覆盖。

## 注意事项

* **永远没有数字。** 这个面对容量只回答 `soldOut: true/false`。实时数字属于开放 API 的 `availability`,在 Secret 密钥之后。
* **404,不是 403。** 草稿活动、别的门户的活动、不存在的 id,从这一侧看完全无法区分。
* **限流保护的是邮件通道。** OTP 请求在按地址节流之外,还按密钥、按访客 IP 双重限流;订单号读取沿用托管门户的按 IP 防线。
* **Storefront 流量进请求日志,不进审计日志。** 调用会出现在控制台的开发者日志里供调试,但 OTP 与下单是*买家*的动作,不写入组织审计日志。履约时自动化照常触发,与其他入口完全同源。

## 相关页面

<CardGroup cols={2}>
  <Card title="API 密钥" icon="key" href="/zh/developers/api-keys">
    创建公开密钥、管理它的 Origin。
  </Card>

  <Card title="Headless 购买流(Secret 密钥)" icon="cart-shopping" href="/zh/developers/checkout">
    服务端形态——当你确实有后端的时候。
  </Card>
</CardGroup>
