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

# 上报计费事件

> POST /v1/events — 上报自定义计费事件（PER_USE，仅 OAuth）

上报自定义计费事件，用于 App 的按次（PER\_USE）计费场景。每次事件上报会根据 Meter 配置从授权用户的积分中扣除相应金额。

```
POST https://api.profy.cn/v1/events
```

## 认证

<Warning>
  此端点**仅支持 OAuth Bearer Token**。API Key 认证会返回 `401` 错误。

  自定义计费事件从用户账户扣除积分，必须通过 OAuth 确保用户知情同意。
</Warning>

```
Authorization: Bearer <access_token>
```

## 请求体

<ParamField body="event" type="string" required>
  事件名称，必须与 App 中配置的 Meter 事件名完全匹配。最大 100 个字符。
</ParamField>

<ParamField body="idempotency_key" type="string" required>
  幂等键，用于防止重复扣费。相同的 `idempotency_key` 不会被重复计费。最大 128 个字符。
</ParamField>

<ParamField body="metadata" type="object">
  附加元数据，键值对形式。用于记录额外的业务信息，不影响计费。
</ParamField>

## 请求示例

<CodeGroup>
  ```python Python theme={null}
  from profy import Profy

  client = Profy(api_key="<oauth_access_token>")

  result = await client.events.report(
      "image_generation",
      idempotency_key="order_12345_gen_1",
      metadata={"style": "watercolor", "resolution": "1024x1024"},
  )
  print(f"扣费: {result['charged']} 积分")
  print(f"剩余: {result['balance_remaining']} 积分")
  ```

  ```typescript TypeScript theme={null}
  import { Profy } from "@profy-ai/sdk";

  const client = new Profy({ apiKey: "<oauth_access_token>" });

  const result = await client.events.report("image_generation", {
    idempotencyKey: "order_12345_gen_1",
    metadata: { style: "watercolor", resolution: "1024x1024" },
  });

  console.log(`扣费: ${result.charged} 积分`);
  console.log(`剩余: ${result.balance_remaining} 积分`);
  ```

  ```bash curl theme={null}
  curl -X POST https://api.profy.cn/v1/events \
    -H "Authorization: Bearer <oauth_access_token>" \
    -H "Content-Type: application/json" \
    -d '{
      "event": "image_generation",
      "idempotency_key": "order_12345_gen_1",
      "metadata": {
        "style": "watercolor",
        "resolution": "1024x1024"
      }
    }'
  ```
</CodeGroup>

## 响应

### 正常扣费

```json theme={null}
{
  "charged": 50,
  "balance_remaining": 9450
}
```

| 字段                  | 类型       | 说明       |
| ------------------- | -------- | -------- |
| `charged`           | `number` | 本次扣除的积分数 |
| `balance_remaining` | `number` | 扣费后的剩余积分 |

### 幂等重放

当同一个 `idempotency_key` 被重复提交时，不会重复扣费：

```json theme={null}
{
  "charged": 50,
  "balance_remaining": 9450,
  "idempotent_replay": true
}
```

`idempotent_replay: true` 表示这是一次幂等重放，积分未被重复扣除。

## 幂等性保障

<Tip>
  始终使用有意义的幂等键。推荐格式：`{业务实体}_{业务ID}_{操作}`，例如 `order_12345_gen_1`。
</Tip>

* **防止网络重试导致的重复扣费** — 客户端超时重试时，相同的幂等键保证只扣一次
* **防止业务逻辑重复调用** — 同一个业务操作多次调用只会扣一次
* **幂等窗口** — 同一 `(app_uuid, idempotency_key)` 组合永久有效

## 工作原理

```
你的 App → POST /v1/events → 查找 Meter → 扣除积分 → 返回结果
                                  ↑
                            App 配置的计量器
                          (event_name → price)
```

1. 你的 App 通过 OAuth 获取用户授权
2. 用户使用付费功能时，调用 Events API 上报事件
3. 袋袋根据 App 配置的 Meter 查找对应的积分价格
4. 从授权用户的积分中扣除对应金额
5. 返回扣费结果和剩余余额

## 每日消费上限

平台为每个用户在每个 App 上设置了每日消费上限。达到上限时后续事件上报会被拒绝（HTTP 429）。具体上限值以产品内实时显示为准。

## 错误码

| HTTP 状态码 | 错误类型                 | 说明                         |
| -------- | -------------------- | -------------------------- |
| `400`    | `invalid_request`    | 缺少必填字段或字段超长                |
| `400`    | `unknown_event`      | 未找到对应的 Meter 配置            |
| `400`    | `meter_disabled`     | 对应的 Meter 已被禁用             |
| `401`    | `unauthorized`       | 需要 OAuth Token，不支持 API Key |
| `429`    | `daily_cap_exceeded` | 用户每日消费上限已达到                |

## 下一步

<CardGroup cols={2}>
  <Card title="计量器配置" icon="gauge" href="/zh/developers/api/meters">
    查询 App 的 Meter 配置
  </Card>

  <Card title="构建袋袋 App" icon="grid-2" href="/zh/developers/app-development">
    完整的 App 开发流程
  </Card>
</CardGroup>
