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

# 私有 CDN

> 面向媒体不公开的企业组织的签名上传与读取 URL。

企业组织可以将鉴定照片和证书存放在公开 CDN 之外。上传写入私有存储桶，读取返回 CloudFront 签名 URL。公开分享页不会显示照片。

创建、读取和提交服务请求仍使用原来的合作伙伴 API。变化的只是上传路径和媒体 URL 的形态。

如果您的组织使用公开 CDN，请使用标准[工作流程](/cn/partner/workflow)的上传路径（`GET /intent`）。

## 所需范围

您的 API 密钥除 `sr:v2:create` 和 `sr:v2:read` 外，还必须包含 `media:v2:private-read`。当组织启用私有媒体时，Legitmark 会授予此范围。

以下所有调用都使用同一把 `leo_` 密钥：

```javascript theme={null}
const headers = {
  'Authorization': 'Bearer leo_xxxxxxxxx',
  'Content-Type': 'application/json'
};
```

没有 `media:v2:private-read` 时，私有 CDN 组织的 `GET /api/v2/sr/{sr_uuid}` 会返回空的 `media_url` 数组，且 `certificate_url` 为 null。

## 集成步骤

与公开合作伙伴 API 相同，只有两处变化：不要调用 `/intent`，并将每条媒体 URL 视为短期有效。

<Steps>
  <Step title="创建服务请求">
    使用 `leo_` 密钥和常规请求体调用 `POST /api/v2/sr?sides=true&item=true`。传入 `sides=true&item=true` 会在响应中直接返回必需/可选照片列表，上传前无需再请求一次。

    **API 参考：** [`POST /api/v2/sr`](/api-reference#tag/Service-Requests/operation/createServiceRequest)
  </Step>

  <Step title="上传照片">
    对所有面调用一次 `POST /api/media/signed-urls/{sr_uuid}`，然后将每张图片 `PUT` 到返回的 `signed_url`。**不要**调用 `GET /intent`。
  </Step>

  <Step title="读取照片">
    使用同一密钥调用 `GET /api/v2/sr/{sr_uuid}?item=true&sides=true`。`media_url`（以及传入 `summary=true` 时的 `summary.thumbnail`）是有效约 15 分钟的私有 CDN 签名链接。
  </Step>

  <Step title="读取证书">
    使用最新 Get Full SR 返回的 `certificate_url`。如果收到 `Access Denied` 或 `403`，请重新获取。也可以在控制台通过 **Download certificate** 重新生成。
  </Step>
</Steps>

## 上传照片

**不要**调用 `GET https://asset.legitmark.com/intent`（以及开发和预发布的资产主机）。该路径写入公开存储桶，无法路由私有组织的上传。

一次请求获取所有面的上传 URL，然后将图片字节 `PUT` 到每个 URL。

**API 参考：** [`POST /api/media/signed-urls/{sr_uuid}`](/api-reference#tag/Media-Management/operation/generateBatchSignedUrls)

<CodeGroup>
  ```javascript HTTP theme={null}
  const sides = requiredSides.map((side) => ({
    uuid: side.uuid,
    filename: `${side.uuid}.jpg`,
    content_type: 'image/jpeg'
  }));

  const signedResponse = await fetch(
    `https://api.legitmark.com/api/media/signed-urls/${srUuid}`,
    {
      method: 'POST',
      headers,
      body: JSON.stringify({
        sides,
        expires_in: 900
      })
    }
  );
  const { urls } = await signedResponse.json();

  for (const side of requiredSides) {
    const { signed_url } = urls[side.uuid];
    await fetch(signed_url, {
      method: 'PUT',
      body: imageFiles[side.uuid],
      headers: { 'Content-Type': 'image/jpeg' }
    });
  }
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.legitmark.com/api/media/signed-urls/SR_UUID" \
    -H "Authorization: Bearer leo_xxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "sides": [
        {
          "uuid": "SIDE_UUID",
          "filename": "SIDE_UUID.jpg",
          "content_type": "image/jpeg"
        }
      ],
      "expires_in": 900
    }'

  curl -X PUT "SIGNED_URL_FROM_ABOVE" \
    -H "Content-Type: image/jpeg" \
    --data-binary @photo.jpg
  ```
</CodeGroup>

**请求体**

| 字段           | 类型      | 必需 | 说明                                                                                                     |
| ------------ | ------- | -- | ------------------------------------------------------------------------------------------------------ |
| `sides`      | array   | 是  | 每张照片一条。每项需要 `uuid`（面 UUID）和 `filename`（`{side_uuid}.jpg`）。`content_type` 可选（`image/jpeg`、`image/png`）。 |
| `expires_in` | integer | 否  | 上传 URL 有效期（秒）。默认 `900`（15 分钟）。                                                                         |

**响应**

```json theme={null}
{
  "success": true,
  "message": "Batch signed URLs generated successfully.",
  "urls": {
    "ec883c70-f896-47c9-81b6-8d6ff0b5856a": {
      "signed_url": "https://s3.us-east-1.amazonaws.com/...",
      "expires_at": "2026-09-01T08:15:00.000Z",
      "is_multipart": false
    }
  },
  "metadata": {
    "total_urls": 1,
    "failed_urls": 0,
    "sr_uuid": "8e61c991-553a-4a58-9eb6-561b64c11908",
    "expires_in": 900
  }
}
```

<Warning>
  向 `signed_url` 发送 `PUT` 时不要携带 `Authorization` 头。该 URL 已经过签名，如果存在 `Authorization` 头，S3 会以 `400` 拒绝请求。只有 `POST /api/media/signed-urls/{sr_uuid}` 需要使用 API 密钥认证。
</Warning>

<Note>
  TypeScript SDK 目前仍使用 `GET /intent`。私有 CDN 组织应通过 HTTP 调用 `POST /api/media/signed-urls/{sr_uuid}`，直到 SDK 更新。
</Note>

### 浏览器上传（CORS）

如果仓库应用或 Web 应用从浏览器 `PUT` 图片，S3 必须允许该页面的来源。请将所有上传来源告知 Legitmark（精确来源和 `https://*.example.com` 通配符）。组织中已放行的来源可继续使用。`PUT` 出现 CORS 错误说明需要添加新来源 — 不要回退到 `/intent`。

## 读取照片

使用同一把 API 密钥获取服务请求。`media_url` 是私有 CDN 主机上的 CloudFront 签名链接。

**API 参考：** [`GET /api/v2/sr/{sr_uuid}`](/api-reference#tag/Service-Requests/operation/getServiceRequestV2)

<CodeGroup>
  ```javascript HTTP theme={null}
  const response = await fetch(
    `https://api.legitmark.com/api/v2/sr/${srUuid}?item=true&sides=true`,
    { headers }
  );
  const { sr } = await response.json();

  for (const side of sr.sides.required) {
    const latest = side.media_url.find((media) => media.is_latest);
    if (latest) {
      console.log(`${side.name}: ${latest.url}`);
    }
  }
  ```

  ```bash cURL theme={null}
  curl "https://api.legitmark.com/api/v2/sr/SR_UUID?item=true&sides=true" \
    -H "Authorization: Bearer leo_xxxxxxxxx"
  ```
</CodeGroup>

签名读取 URL 形如（查询值已截断）：

```
https://staging-{org}-private-cdn.legitmark.com/org/{org_uuid}/sr/{sr_uuid}/{side_uuid}.jpg?Expires=1787907356&Key-Pair-Id=...&Signature=...
```

* 保留 URL 上的 `Expires`、`Key-Pair-Id` 和 `Signature`。去掉查询字符串会返回 `403`。
* 传入 `summary=true` 时，`summary.thumbnail` 以同样方式签名。
* 早期测试中的公开 `cdn.legitmark.com` 链接不再提供这些照片。

## 证书

最新 `GET /api/v2/sr/{sr_uuid}` 返回的 `certificate_url` 是同一私有主机上的签名 PDF 链接。请使用该 URL。如果收到 `Access Denied` 或 `403`，说明签名已过期 — 再次调用 `GET` 并使用新 URL。

也可以在控制台（`https://app.legitmark.com` 或开发/预发布应用主机）通过 **Download certificate** 重新生成新链接。

不要复用之前保存的证书链接。

## URL 有效期

签名读取 URL 自签发时起有效 **15 分钟**（`expires_in` 默认 `900` 秒）。

* 不要将签名 URL 持久化或缓存在自己的数据库中。
* 不要把签名 URL 当作长期分享链接写入日志或转发给他人。持有该 URL 的人在过期前都能获取对象。
* 每次需要显示或下载照片或证书时，重新调用 `GET /api/v2/sr/{sr_uuid}`。
* 过期或未签名的 URL 返回 `403`。这是预期行为。请获取新的 URL。

Webhook 负载不包含媒体 URL。收到 `state_change` 后，如需照片或证书，请再获取 SR。

## 公开分享页

公开分享页不会显示私有 CDN 组织的照片。这是预期行为。照片和证书仅可通过带密钥的合作伙伴 API 访问。

| 环境  | 分享页                                                   |
| --- | ----------------------------------------------------- |
| 开发  | `https://dev.app.legitmark.com/view?sr={sr_uuid}`     |
| 预发布 | `https://staging.app.legitmark.com/view?sr={sr_uuid}` |
| 生产  | `https://app.legitmark.com/view?sr={sr_uuid}`         |

## 环境

| 环境  | API 主机                              | 私有 CDN 主机示例                                       |
| --- | ----------------------------------- | ------------------------------------------------- |
| 开发  | `https://dev.api.legitmark.com`     | `https://dev-{org}-private-cdn.legitmark.com`     |
| 预发布 | `https://staging.api.legitmark.com` | `https://staging-{org}-private-cdn.legitmark.com` |
| 生产  | `https://api.legitmark.com`         | `https://{org}-private-cdn.legitmark.com`         |

将 `{org}` 替换为 Legitmark 为您的组织预配的主机名。请使用 API 返回的 URL，不要自行拼接 CDN 主机。

## 图片要求

与公开工作流程相同：

* **格式：** JPG/JPEG/PNG
* **尺寸：** 最小 600 x 600 像素
* **文件大小：** 每张图片最大 5 MB
* **质量：** 清晰、光线充足、对焦准确的图片
