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

# Usage webhooks

> รับทุกการเรียกที่คิดเงินเข้าระบบของคุณ พร้อมวิธีตรวจสอบว่ามาจาก Prism จริง

# Usage webhooks

Prism ส่ง event ที่เซ็นแล้วไปยัง endpoint ของคุณทุกครั้งที่มีการเรียกที่คิดเงิน
ใช้กระทบยอดค่าใช้จ่าย ทำ chargeback ภายใน หรือป้อนแดชบอร์ดของคุณเองได้ โดยไม่ต้อง
poll usage API

ตั้งค่าเองได้ในคอนโซลที่ **Team → Usage webhook** ระบบจะสร้าง signing secret ให้
และแสดงครั้งเดียวตอนสร้าง

## สิ่งที่ส่งมา

หนึ่ง `POST` ต่อหนึ่ง usage event, `content-type: application/json`:

```json theme={null}
{
  "type": "usage.event.created",
  "data": {
    "event_id": "0f7c…",
    "occurred_at": "2026-08-12T09:14:22.104Z",
    "org_id": "9b2e…",
    "api_key_id": "3ad1…",
    "public_model_id": "openai/gpt-5.5",
    "channel": "chat",
    "billed_units": { "input_tokens": 812, "output_tokens": 241 },
    "cost_credits_minor": 143,
    "upstream_cost_micro_usd": 21400,
    "request_id": "req_…",
    "cache_hit": false,
    "status": "ok"
  }
}
```

`cost_credits_minor` เป็นหน่วยย่อย — `143` คือ ฿1.43

Header:

```text theme={null}
webhook-id:        <ไม่ซ้ำต่อหนึ่ง delivery>
webhook-timestamp: <unix seconds>
webhook-signature: v1,<base64 hmac-sha256>
x-prism-event-type: usage.event.created
```

เป็นรูปแบบ [Standard Webhooks](https://www.standardwebhooks.com/) ดังนั้น
verifier ของ Standard Webhooks ที่มีอยู่แล้วใช้ได้เลย

## การตรวจสอบลายเซ็น

ข้อความที่เซ็นคือ `{webhook-id}.{webhook-timestamp}.{raw body}`

<Warning>
  ต้องเซ็นจาก **raw request body** ก่อน parse JSON การแปลง object ที่ parse แล้ว
  กลับเป็น JSON ใหม่ทำให้ลำดับ key และช่องว่างเปลี่ยน ลายเซ็นจะไม่ตรง
</Warning>

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyPrismWebhook(headers, rawBody, secret) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  const sigHeader = headers["webhook-signature"] ?? "";

  // ตรวจเวลาก่อน — ลายเซ็นถูกต้องบน body เก่าหกเดือนก็ยังเป็น replay
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${ts}.${rawBody}`)
    .digest("base64");

  // header อาจมีหลายลายเซ็นคั่นด้วยช่องว่างระหว่างช่วงหมุน secret
  return sigHeader.split(" ").some((part) => {
    if (!part.startsWith("v1,")) return false;
    const got = Buffer.from(part.slice(3), "utf8");
    const want = Buffer.from(expected, "utf8");
    return got.length === want.length && timingSafeEqual(got, want);
  });
}
```

ตัวอย่าง Python และ Go อยู่ใน[ฉบับภาษาอังกฤษ](/usage-webhooks)

## การตอบกลับ

ตอบ `2xx` ทันทีที่บันทึก event แล้ว งานที่เหลือค่อยทำต่อ — handler ที่ช้าจะกิน
โควตา retry ของคุณเอง

สถานะอื่นถือว่าล้มเหลวและจะ retry แบบ exponential backoff (1 นาที, 2, 4, 8 …
สูงสุด 1 ชั่วโมง) จนครบจำนวนครั้ง แล้วจึงมาร์กเป็น `dead` และแสดง error ล่าสุด
ในคอนโซล

## Idempotency

การ retry ทำให้ `event_id` เดิมมาถึงได้มากกว่าหนึ่งครั้ง ให้ใช้ `event_id` เป็น
primary key ฝั่งคุณและข้ามตัวซ้ำ อย่าบวกยอดสะสมทุกครั้งที่ได้รับ

## ลำดับ

delivery ไม่รับประกันลำดับ event ที่ retry อาจมาถึงหลัง event ที่ใหม่กว่า
ถ้าลำดับสำคัญให้ใช้ `occurred_at` แทนลำดับการมาถึง

## การหมุน secret

secret แสดงครั้งเดียวและเก็บแบบเข้ารหัส Prism แสดงให้อีกไม่ได้ ถ้าจะหมุน ให้
เพิ่ม endpoint ตัวที่สองที่ URL เดิม deploy ตัวรับที่ยอมรับได้ทั้งสอง secret
แล้วค่อยลบตัวเก่า

[English version](/usage-webhooks)
