ให้ AI เขียน integration ให้
วางสองอย่างนี้ให้ Claude, Cursor หรือ ChatGPT ก่อนถามคำถามแรก — prompt สั้น ๆ ที่บอกกฎที่ integration พังบ่อยที่สุด และเอกสารทั้งหน้าในรูป Markdown
You are helping me integrate the Donato.click donation API.
Signed: IJe-Donato-click v1.0.1-1
Base URL: https://donato.click/api/v1
Sandbox: https://sandbox-api.donato.click/api/v1
Full machine-readable schema: https://donato.click/api/v1/openapi.json
Auth: every request sends `Authorization: Bearer $DONATO_API_KEY`.
Rules that decide whether the integration is correct:
1. Webhooks are the truth. A 201 from POST /donations means "payment requested",
not "money arrived". Only the `donation.completed` webhook means paid.
2. Money is satang (integer). 10000 = ฿100.00. Never parse amounts as floats.
3. Delivery guarantee: at-least-once — deduplicate on event_id
4. Verify every webhook before trusting it:
- header: `X-IJe-Donato-click-Signature`
- format: t=<unix-seconds>,v1.0.1-1=<hex hmac-sha256>
- signed payload: `${timestamp}.${rawBody}` — sign the RAW body, not a re-serialised object.
- reject anything older than 300s, and compare digests
in constant time.
5. Reply 2xx within 10s. Failed attempts retry on
1m → 5m → 15m → 1h → 24h, and an endpoint is disabled
after 5 consecutive failures.
6. Send an `Idempotency-Key` on every write.
Webhook events: donation.completed, donation.refunded, withdraw.completed, ping.
Tooling you can use instead of writing HTTP by hand: the `donatoclick` CLI
(`donato init` scaffolds a receiver that already verifies signatures) and the
`donatoclick-mcp` MCP server, which can create endpoints and send test events.
When you are unsure about a route, read openapi.json rather than guessing.ทั้ง prompt และ Markdown generate จาก openapi.json, รายการ event และ retry schedule ตัวเดียวกับที่หน้านี้ใช้ — โค้ดเปลี่ยนเมื่อไหร่ ข้อความที่คัดลอกก็เปลี่ยนตาม ไม่มีทางบอก AI ว่า header ลายเซ็นชื่ออะไรผิด · ถ้าอยากให้ agent ลงมือทำเองได้เลย (สร้าง endpoint, ยิง test, ไล่ event log) ใช้ MCP server ที่ 4.2 CLI & MCP · หรือพา AI ไปดูภาพรวมก่อนที่ terminal tour
สองข้อที่ตัดสินว่า integration ถูกหรือผิด
ทุกอย่างที่เหลือคือรายละเอียด สองข้อนี้คือที่ที่ integration พังจริง — ภาพเดียวกันกับที่ terminal tour สอนด้วย donato check
1. Webhook คือความจริง
201 จาก POST /donations แปลว่า “ขอให้จ่าย” ไม่ใช่ “เงินเข้าแล้ว” รอ donation.completed เท่านั้น · ดู event catalogue →
2. เงินเป็นสตางค์จำนวนเต็ม
10000 = ฿100.00 อย่า parse เป็น float เด็ดขาด ปัดเศษครั้งเดียวก็ยอดไม่ตรงแล้ว
curl https://donato.click/api/v1/usage \ -H "Authorization: Bearer dnt_live_..."
ลองคำสั่งเดียวกันใน shell ได้ที่ cli → overview
Authentication
Bearer token ธรรมดา แต่มีสองมิติที่พลาดกันบ่อย: environment ผูกกับ host ไม่ใช่กับ header และ scope ผูกกับ key ตอนสร้าง
| Prefix | Host | ใช้ทำอะไร |
|---|---|---|
| dnt_test_ | https://sandbox-api.donato.click/api/v1 | Sandbox — ทุกแพลนใช้ได้ |
| dnt_live_ | https://donato.click/api/v1 | Production — ต้องแพลน Rise ขึ้นไป |
เอา test key ไปยิง production host จะได้ 403 พร้อม type sandbox_only — จงใจให้เป็นแบบนี้ เพราะถ้าปล่อยให้สลับ environment ด้วย header หรือ query ได้ สุดท้ายจะมีคนเข้าใจว่าโดเนทจริงเป็นของทดสอบ
Scope มีสามระดับ: read · write · withdraw — ให้เท่าที่ต้องใช้ ไม่ต้องให้ครบ
curl https://donato.click/api/v1/donations \ -H "Authorization: Bearer $DONATO_API_KEY"
สร้าง key จริงที่ dashboard → developers
Endpoint reference
33 operations — อ่านตรงจาก openapi.json ที่ server เสิร์ฟอยู่
/analytics
get/analytics/overviewRevenue KPIsread
Clamped to the plan analytics retention window.
Parameters
months· query
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/analytics/overview" \ -H "Authorization: Bearer $DONATO_API_KEY"
/creator
get/creator/profileThe key owner's profileread
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/creator/profile" \ -H "Authorization: Bearer $DONATO_API_KEY"
/custom-domains
get/custom-domainsList custom domainsread
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/custom-domains" \ -H "Authorization: Bearer $DONATO_API_KEY"
patch/custom-domainsVerify or remove a custom domainwrite
The id travels in the body, not the path. Action `verify` checks DNS and `remove` deletes.
Responses
200— Verified or removed400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X PATCH "https://donato.click/api/v1/custom-domains" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'post/custom-domainsAdd a custom domainwrite
Rise+ only. Max 3.
Responses
201— Domain added400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X POST "https://donato.click/api/v1/custom-domains" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'/donation-urls
get/donation-urlsList donation linksread
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/donation-urls" \ -H "Authorization: Bearer $DONATO_API_KEY"
patch/donation-urlsUpdate a donation linkwrite
Two shapes, both accepted. The `action` shape updates one thing per request; the multi-field shape updates everything in one request. Never mix `action: revoke` with other fields. min_amount is in BAHT here (the server multiplies by 100) — unlike the rest of the API, which counts integer satang. Tech debt: unify on satang.
Responses
200— Updated400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X PATCH "https://donato.click/api/v1/donation-urls" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'post/donation-urlsCreate a donation linkwrite
Rise+ only. Always starts offline — toggle online requires verification.
Responses
201— Link created400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X POST "https://donato.click/api/v1/donation-urls" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'get/donation-urls/check-slugCheck whether a custom slug is available
Public — no API key needed. Rate-limited.
Parameters
slug· queryrequired
Responses
200— Availability result400— Missing slug
curl -X GET "https://donato.click/api/v1/donation-urls/check-slug" \ -H "Authorization: Bearer $DONATO_API_KEY"
/donations
get/donationsList donationsread
Newest first, cursor-paginated.
Parameters
limit· querycursor· query — Opaque cursor from a previous response. There is no offset paging.status· queryfrom· queryto· query
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/donations" \ -H "Authorization: Bearer $DONATO_API_KEY"
post/donationsCreate a donation requestwrite
Returns a payment URL. NOT a confirmation of payment — wait for the donation.completed webhook.
Parameters
Idempotency-Key· headerrequired — A UUID you generate per intent. Retrying with the same key replays the first response for 24h.
Responses
201— Donation request created400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production409— Idempotency-Key reused with a different body422— Amount outside the allowed range429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X POST "https://donato.click/api/v1/donations" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'get/donations/{id}Get one donationread
Parameters
id· pathrequired
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/donations/{id}" \
-H "Authorization: Bearer $DONATO_API_KEY"get/donations/statisticsDonation statisticsread
Totals bucketed by day, week, or month. Counts only revenue-bearing statuses, matching the creator dashboard.
Parameters
period· queryfrom· queryto· query
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/donations/statistics" \ -H "Authorization: Bearer $DONATO_API_KEY"
/events
get/eventsEvent log (catch-up feed)read
Every event this account produced, oldest first. Use it to recover events missed while your server was down: persist the last event_id you processed and pass it as `after`.
Parameters
after· query — Last event_id you processed.event· querylimit· query
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/events" \ -H "Authorization: Bearer $DONATO_API_KEY"
/goals
delete/goalsDelete a goalwrite
The id travels as `?id=`, not as a path segment — there is no /goals/{id} route.
Parameters
id· queryrequired
Responses
200— Deleted400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X DELETE "https://donato.click/api/v1/goals" \ -H "Authorization: Bearer $DONATO_API_KEY"
get/goalsList goalsread
All goals for the key owner.
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/goals" \ -H "Authorization: Bearer $DONATO_API_KEY"
patch/goalsUpdate a goalwrite
The id travels in the body, not the path — there is no /goals/{id} route.
Responses
200— Updated400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X PATCH "https://donato.click/api/v1/goals" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'post/goalsCreate a goalwrite
Rise+ only — free is read-only. Idempotent by name+slug.
Responses
201— Goal created400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X POST "https://donato.click/api/v1/goals" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'/keys
get/keysList API keysread
Metadata only. A key is never readable after it is issued.
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/keys" \ -H "Authorization: Bearer $DONATO_API_KEY"
post/keysCreate an API key — not available to API keyswrite
Returns 501. A key that could mint keys would survive revoking the original, so issuing is a dashboard-session operation.
Responses
400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).501— Dashboard session required
curl -X POST "https://donato.click/api/v1/keys" \ -H "Authorization: Bearer $DONATO_API_KEY"
delete/keys/{id}Revoke an API keywrite
Idempotent, and allowed where creation is not: revoking narrows access. A key may revoke itself, after which the next call with it returns 401.
Parameters
id· pathrequired
Responses
200— Revoked400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X DELETE "https://donato.click/api/v1/keys/{id}" \
-H "Authorization: Bearer $DONATO_API_KEY"/presets
delete/presetsDelete a widget presetwrite
The id travels in the ?id= query param — there is no /presets/{id} route.
Parameters
id· queryrequired
Responses
200— Deleted400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X DELETE "https://donato.click/api/v1/presets" \ -H "Authorization: Bearer $DONATO_API_KEY"
get/presetsList widget presetsread
Empty canvas by default.
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/presets" \ -H "Authorization: Bearer $DONATO_API_KEY"
patch/presetsUpdate a widget presetwrite
The id travels in the body, not the path — there is no /presets/{id} route.
Responses
200— Updated400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X PATCH "https://donato.click/api/v1/presets" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'post/presetsCreate a widget preset (blank)write
Rise+ only — free is read-only. Empty canvas_elements.
Responses
201— Preset created400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X POST "https://donato.click/api/v1/presets" \ -H "Authorization: Bearer $DONATO_API_KEY"
/usage
get/usageCurrent limits and usageread
Exempt from the quotas it reports, so it stays callable when a quota is exhausted.
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/usage" \ -H "Authorization: Bearer $DONATO_API_KEY"
/wallet
get/walletWallet balanceread
Balances in satang.
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/wallet" \ -H "Authorization: Bearer $DONATO_API_KEY"
/webhooks
get/webhooksList webhook endpointsread
Signing secrets are never included — they are returned once, by POST, and never again.
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/webhooks" \ -H "Authorization: Bearer $DONATO_API_KEY"
post/webhooksRegister a webhook endpointwrite
Returns the signing secret ONCE, in `data.secret`. Store it before you read anything else in the response. The environment is decided by the host you called, not by the body.
Responses
201— Endpoint registered; `data.secret` is present exactly this once400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X POST "https://donato.click/api/v1/webhooks" \
-H "Authorization: Bearer $DONATO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'delete/webhooks/{id}Delete a webhook endpointwrite
Soft delete — the delivery log stays readable for its retention window.
Parameters
id· pathrequired
Responses
200— Deleted400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production404— No such endpoint429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X DELETE "https://donato.click/api/v1/webhooks/{id}" \
-H "Authorization: Bearer $DONATO_API_KEY"post/webhooks/{id}/testSend a test eventwrite
Delivers a `ping` inline and returns the receiver's own status code and body. A non-2xx there is the answer, not an error on our side.
Parameters
id· pathrequired
Responses
200— Attempted — see `data.delivered`400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production404— No such endpoint429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X POST "https://donato.click/api/v1/webhooks/{id}/test" \
-H "Authorization: Bearer $DONATO_API_KEY"/withdrawals
get/withdrawalsWithdrawal historyread
Parameters
limit· querycursor· query — Opaque cursor from a previous response. There is no offset paging.
Responses
200— Success400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).
curl -X GET "https://donato.click/api/v1/withdrawals" \ -H "Authorization: Bearer $DONATO_API_KEY"
post/withdrawalsCreate a withdrawal — not available to API keyswithdraw
Returns 501. Withdrawals require a step-up (AAL2) check that an API key cannot present. Create them from the dashboard.
Responses
400— Invalid request401— Invalid or revoked API key403— Insufficient scope, plan, or a sandbox-only key aimed at production429— Rate limit (type ends with 'rate_limited' — retry after the window) or monthly quota (type ends with 'quota_exceeded' — retrying will not help).501— Step-up authentication required
curl -X POST "https://donato.click/api/v1/withdrawals" \ -H "Authorization: Bearer $DONATO_API_KEY"
Webhook event catalogue
สมัครไว้กี่ event ก็ได้ ไม่เลือกเลย = รับทุกอัน การันตีแบบ at-least-once ให้ dedupe ด้วย event_id เสมอ
donation.refunded คือ event เดียวที่ “ถอนคืน” สิ่งที่ donation.completed เคยบอกไว้ — ยิงเมื่อเงินถูกคืนให้ผู้โดเนท หรือถูก dispute โดยธนาคาร ถ้า integration ของคุณปลดล็อครางวัลตอน donation.completed ต้องมี path ที่เก็บคืนด้วย data.reason บอกว่า refund หรือ disputeทุก delivery คือ POST ที่ body หน้าตาแบบนี้ และมาพร้อม header X-Donato-Event, X-Donato-Event-Id, X-Donato-Delivery-Attempt และ X-IJe-Donato-click-Signature · เห็นภาพเดียวกันแบบสั้น ๆ ที่ cli → webhooks
{
"event": "donation.completed",
"event_id": "evt_...",
"created_at": "2026-01-31T09:12:44.120Z",
"data": {
"amount": 10000,
"currency": "THB",
"donor_name": "...",
"message": "..."
}
}event_id ที่ประมวลผลแล้วไว้ แล้วข้ามตัวซ้ำ อย่าใช้เวลาที่ได้รับเป็นตัวตัดสินSignature verification
Header X-IJe-Donato-click-Signature หน้าตาเป็น t=<unix-seconds>,v1.0.1-1=<hex hmac-sha256> — ต้อง sign กับ raw body เท่านั้น
สิ่งที่ถูก sign คือ `${timestamp}.${rawBody}` — sign the RAW body, not a re-serialised object. ที่ต้องมี timestamp อยู่ในนั้นเพราะลายเซ็นที่ครอบแค่ body จะ valid ตลอดกาล — ใครดัก delivery ได้ครั้งเดียวก็ replay ได้ตลอดชีวิต ฝั่งรับต้องปฏิเสธอะไรที่เก่ากว่า 300 วินาที
JSON.stringify ใหม่ ลายเซ็นจะไม่ตรงทันที เพราะ byte ไม่เหมือนเดิม ต้องอ่าน raw bodyimport { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = new Map(
header.split(',').map(p => {
const [k, v] = p.split('=');
return [k?.trim() ?? '', v?.trim() ?? ''] as const;
})
);
const timestamp = Number(parts.get('t'));
const provided = parts.get('v1.0.1-1');
if (!Number.isFinite(timestamp) || !provided) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(provided, 'hex');
// เช็คความยาวก่อน: timingSafeEqual โยน exception ถ้ายาวไม่เท่ากัน
// และ exception ที่โยนออกมาก็เป็น timing signal ในตัวมันเอง
return a.length === b.length && timingSafeEqual(a, b);
}เริ่ม receiver ใหม่พร้อม verify ที่ถูกต้องด้วย donato init — ดูที่ cli → scaffold fast
Retry & auto-disable
Timeout 10 วินาทีต่อครั้ง ลองใหม่ 5 รอบ แล้วหยุด
นับว่าสำเร็จเมื่อได้ 2xx เท่านั้น ถ้า endpoint พลาดติดกัน 5 ครั้ง ระบบจะปิดให้อัตโนมัติ แล้วขึ้นเหตุผลไว้ในหน้า Developers — เปิดใหม่ได้เมื่อแก้เสร็จ (การเปิดใหม่จะรีเซ็ตตัวนับให้ด้วย)
ถ้าปิดไปแล้วหรือ server ล่มนานกว่า retry window อย่ามานั่งเดาว่าพลาดอะไรไป — ใช้ /events ดึงย้อนหลัง
Catch-up ด้วย GET /events
Webhook คือ push, endpoint นี้คือ pull — มีไว้สำหรับตอนที่ server คุณล่มไปสองชั่วโมงแล้วโดเนทช่วงนั้นหายไปจากสายตา
เก็บ event_id ตัวสุดท้ายที่ประมวลผลสำเร็จไว้ แล้วส่งเป็น after ตอน start ขึ้นมาใหม่ endpoint นี้เรียงจากเก่าไปใหม่ (ต่างจาก list endpoint อื่นทั้งหมด) เพราะการ replay ต้องเป็นลำดับเดียวกับที่มันเกิดขึ้นจริง
let cursor = await loadLastSeenEventId(); // null ครั้งแรก
let hasMore = true;
while (hasMore) {
const url = new URL('https://donato.click/api/v1/events');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('after', cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.DONATO_API_KEY}` },
});
const { data, meta } = await res.json();
for (const event of data) await handle(event);
cursor = meta.next_after;
hasMore = meta.has_more;
await saveLastSeenEventId(cursor); // save หลัง handle เสมอ ไม่ใช่ก่อน
}Rate limit ของ endpoint นี้ตั้งไว้เป็นสองเท่าของ read ปกติ เพราะมันถูกออกแบบมาให้โดนยิงรัว ๆ ตอนที่ทุกอย่างพังพอดี · ลองแบบไม่ต้องเขียนโค้ดที่ cli → events tail
Limits
ตัวเลขชุดนี้อ่านจาก constant เดียวกับที่ server บังคับใช้จริง
| Rate limit (req/min) | free | rise | peak |
|---|---|---|---|
| sandbox | 60 | 60 | 120 |
| read | — | 60 | 300 |
| write | — | 20 | 100 |
| withdrawal | — | — | 5 |
| events | — | 120 | 600 |
| alerts | — | 30 | 60 |
| realtime | — | 10 | 30 |
| promptpay | — | 60 | 300 |
| slip | — | 10 | 30 |
| Quota | free | rise | peak |
|---|---|---|---|
| API keys | 1 | 2 | 10 |
| Webhook endpoints | 1 | 3 | 10 |
| Webhook replays / month | 0 | 0 | 100 |
| Webhook log retention (days) | 1 | 7 | 30 |
| Event log retention (days) | 1 | 30 | 90 |
โดน 429 แล้วดู type ใน problem ให้ดี: ลงท้ายด้วย rate_limited คือรอแล้วลองใหม่ได้, ลงท้ายด้วย quota_exceeded คือลองใหม่ไม่ช่วย ต้องรอรอบบิลหรืออัปเกรด
CLI & MCP server
ของสองอย่างนี้ใช้ client layer ตัวเดียวกัน — อันหนึ่งให้คนพิมพ์ อีกอันให้ agent เรียก · ภาพรวมแบบ 60 วินาทีอยู่ที่ terminal tour
donato CLIdonatoclick
package ชื่อ donatoclick คำสั่งที่ติดตั้งมาชื่อ donato — Node 20 ขึ้นไป ไม่ต้องลง dependency อะไรเพิ่ม · ดูคำสั่งจริงใน terminal →
npm install -g donatoclick # หรือไม่ต้องติดตั้ง — ต้องเรียกด้วยชื่อ package npx donatoclick login
npx donato ไม่ใช่ตัวนี้ — donato บน npm เป็นของคนอื่น ชื่อ donato ใช้ได้หลังติดตั้งแล้วเท่านั้น
donato login # เก็บ key ไว้ที่ ~/.donato/config.json donato keys list donato webhooks create https://your-server.com/hook donato webhooks test <id> donato events tail donato listen --forward localhost:3000/api/donato donato init # scaffold receiver ที่ verify ลายเซ็นให้แล้ว
MCP server
ให้ AI agent ตั้ง integration ให้เสร็จเองได้ตั้งแต่ต้นจนจบ — list key, สร้าง endpoint, ยิง test, ไล่ event log · ปล่อยเป็น donatoclick-mcp ไม่ต้องติดตั้งไว้ก่อน client เรียกผ่าน npx ได้เลย · ดู built-for-agents →
{
"mcpServers": {
"donato": {
"command": "npx",
"args": ["-y", "donatoclick-mcp"],
"env": { "DONATO_API_KEY": "dnt_test_..." }
}
}
}พร้อมแล้ว? สร้าง API key แล้วกลับมาเริ่มที่ terminal tour → overview