<!-- Source: https://docs.infraio.xyz/zh-CN/api-reference/authentication -->
<!-- Last updated: 2026-10-04 -->

# 身份验证

InfraIO Pay 有**两套 API 表面**,鉴权模型不同。挑选与调用方匹配的那一套:

| 表面 | 路径前缀 | 受众 | 鉴权 |
| --- | --- | --- | --- |
| **商户 B2B** | `/b2b/v1/*` | 你的服务器 | HMAC-SHA256 请求签名 |
| **仪表板** | 供商户仪表板使用 | 商户仪表板的浏览器会话 | Bearer JWT |

本页面介绍 **B2B** 表面,也就是你用 API 密钥对从服务端调用的那一套。
仪表板表面供 InfraIO Pay 商户仪表板使用,不是公开的集成接口。

请始终发送包含 `/b2b` 前缀的完整路径,并对同一路径签名(见下文)。

## 端点

| 环境 | Base URL |
| --- | --- |
| Test | `https://api-dev.infraio.xyz` |
| Live | `https://api.infraio.xyz` |

URL 模式相同 — 环境由**密钥前缀**(`pk_test_…` vs `pk_live_…`)控制,
而不是 URL。

## 密钥对

你从商户仪表板(**Developers → API keys → + Add key**)获得两个值:

- **Publishable key** (`pk_test_…` 或 `pk_live_…`)— 标识你的账户。
  作为 `X-Client-ID` 发送。可安全嵌入浏览器 bundle(SDK 已经在用)。
- **Secret key** (`sk_test_…` 或 `sk_live_…`)— HMAC 签名密钥。仅限
  服务端。请像数据库密码一样对待。

> **Important:**
>
> 如果 secret key 不慎进入浏览器 bundle、git 仓库、日志行,或被分享
> 到聊天 — **立刻从仪表板吊销**。
> 吊销立即生效,没有重叠窗口。请重新签发并重新部署。

## 对请求签名

每次 `/b2b/v1/*` 调用都携带三个标头:

```http
X-Client-ID:  pk_live_70d0becbd7a53047b0dfe88a960d10ad
X-Timestamp:  1715990400
X-Signature:  9a8b7c6d…             (hex HMAC-SHA256)
```

签名在一个规范化字符串上计算:

```
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODY
```

- `METHOD` — 大写的 HTTP 动词(`POST`、`GET`、……)。
- `PATH` — 请求路径**包含 `/b2b` 前缀**,不含 host 且**不含查询字符串**
  (例如 `/b2b/v1/checkout-sessions/quick`)。前缀必须在。查询参数**不**参与签名 —
  对于 `GET …?cursor=…&limit=20`,只对路径签名,不要包含 `?…` 部分。
- `TIMESTAMP` — Unix 秒,十进制字符串(例如 `"1715990400"`),与
  `X-Timestamp` 完全一致。
- `BODY` — 原始请求 body 字节。`GET`/`DELETE` 为空字符串。

用 **secret** key 作为密钥的 HMAC-SHA256 签名,输出 **hex**:

**Node / TS**

```ts
import { createHmac } from "node:crypto";

function sign({ method, path, body, secret }: {
  method: string; path: string; body: string; secret: string;
}) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const input = [method.toUpperCase(), path, timestamp, body].join("\n");
  const signature = createHmac("sha256", secret).update(input).digest("hex");
  return { timestamp, signature };
}
```

**Go**

```go
package infraio

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strconv"
    "time"
)

func Sign(method, path, body, secret string) (timestamp, signature string) {
    timestamp = strconv.FormatInt(time.Now().Unix(), 10)
    input := method + "\n" + path + "\n" + timestamp + "\n" + body
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(input))
    return timestamp, hex.EncodeToString(mac.Sum(nil))
}
```

**Python**

```python
import hmac, hashlib, time

def sign(method: str, path: str, body: str, secret: str):
    timestamp = str(int(time.time()))
    input_ = f"{method}\n{path}\n{timestamp}\n{body}"
    signature = hmac.new(
        secret.encode(), input_.encode(), hashlib.sha256
    ).hexdigest()
    return timestamp, signature
```

## 为什么选 HMAC,而不是 Bearer?

裸 Bearer-token API 会在每次请求里把你唯一的 secret 经线传输。任何
拿到某个 TLS 终止代理日志的人都能拿到你账户的钥匙。HMAC 签名意味着
secret 永远不上路 — 上路的只是其衍生签名,而且一次性(绑定到该次
请求 + 该分钟)。

代价是:每次调用都要计算签名。目前还没有服务端 SDK,但上面的
helper 每种语言大约 15 行。

## 时间戳容差

容差为 **±5 分钟**(300 秒)。超出该窗口的请求会被以
`401 invalid_signature` 拒绝。两点含义:

1. **同步服务器时钟**到 NTP。时钟漂移的长跑 cron 会间歇失败。
2. **不要预先计算并入队签名。** 如果请求在重试队列里坐了 > 5 分钟,
   它的签名就过期了。

## 密钥 scope

Secret key 携带一个或多个下面的 scope 包:

| Scope | 预期用途 |
| --- | --- |
| `read` | 列出/读取订单、会话、退款 |
| `write_order` | 创建结账会话、订单 |
| `write_refund` | 发起退款、铸造退款申请 token |
| `webhook_manage` | 创建/更新/删除 Webhook 端点 |

仪表板默认签发"全权限"密钥(全部四个 scope)。你可以从
**Developers → API keys → + Add key** 铸造受限 scope 的密钥,仅勾选
集成需要的 scope。

> **Warning:**
>
> **scope 目前尚未强制执行。** scope 会记录在密钥上并显示在仪表板中,
> 但任何有效的 `sk_…` 密钥都可以调用你商户的任意 `/b2b/v1/*` 端点。
> 请勿把 scope 当作安全边界。请通过轮换或吊销密钥来限制访问。

## 验证失败

如果签名、`X-Client-ID` 或时间戳无效,请求会在到达 API 之前被以
**401 `INVALID_SIGNATURE`** 拒绝。只有 `/b2b/v1/*` 请求使用这种方式签名。
Webhook 使用另一套方案(见下文)。

## 下一步

- [错误](https://docs.infraio.xyz/zh-CN/api-reference/errors) — 4xx/5xx 的响应形态。
- [安全 → API 密钥](https://docs.infraio.xyz/zh-CN/security/api-keys) — 轮换、吊销,以及
  secret 泄露时的处理。
- [Webhooks → 签名验证](https://docs.infraio.xyz/zh-CN/webhooks/signature-verification) —
  使用*不同*的 HMAC 方案(标头 `X-Signature: sha256=…`,签名
  `X-Timestamp + "." + raw_body`,加上 24 小时轮换宽限窗口内可选的
  `X-Signature-Prev`)。不要把两种方案搞混 — 它们共用哈希算法,
  但签名字节和 secret 家族(`whsec_…` vs `sk_…`)不同。
