Know the moment someone tips and let your systems take it from there
In plain wordsEvery time a tip is confirmed, Tipjai sends a short message to a service such as Zapier, and you decide what happens next: add it to Google Sheets, post it in Discord, anything.
LimitsFor now the destination must be a Zapier, Make or IFTTT URL. Set it in the dashboard on the automations card, titled เชื่อมต่ออัตโนมัติ (Zapier / Make / IFTTT).
- Log every tip in Google SheetsTake the webhook and add a row: name, amount and message.
- Say thanks in DiscordPost in a channel every time someone tips.
- Flash the room lightsHook up a smart bulb so it flashes on every tip.
Send tips to Google Sheets automatically
No code needed: Make and Zapier both have free plans. If you only need a file for taxes, the CSV download button in the dashboard is simpler.
- 1Sign up for Make or Zapier
- 2Create a new scenario, choose the Webhooks trigger, then Custom webhook, and copy the URL it gives you
- 3Paste that URL into the เชื่อมต่ออัตโนมัติ card in your Tipjai dashboard and press บันทึก (save)
- 4Press ส่งทดสอบ (send a test): the destination gets an event with supporter, amount and message
- 5Add a Google Sheets action, Add a Row, and drag each field into a column
- 6Turn the scenario on. Every tip lands in the sheet by itself.
One request, three layers
This example is signed for real with a sample secret. Check it yourself in the box below.
POST https://hooks.zapier.com/hooks/catch/… Content-Type: application/json; charset=utf-8 User-Agent: Tipjai-Webhook/1.0 Idempotency-Key: 8c2f4e1a-6b3d-4f7e-9a2c-1d5e8f0b3a47 X-Tipjai-Delivery: 8c2f4e1a-6b3d-4f7e-9a2c-1d5e8f0b3a47 X-Tipjai-Event: tip.confirmed X-Tipjai-Timestamp: 1790501464 X-Tipjai-Signature: v1=e8a89c93dfba1464…7ceba520
- The same id again means a retry of the same delivery. Skip it.
- The timestamp is Unix time in seconds, made fresh on every attempt, retries included.
hmac_sha256( secret, "1790501464" + "." + raw body ) → hex, prefixed with v1=
- It signs the raw body only. Never parse and re-serialise before checking.
{ "id": "8c2f4e1a-…-1d5e8f0b3a47",
"data": { "gift": { "id": "rose", "qty": 3, "name": "กุหลาบ" },
"amount": 30, "tip_id": "e7a1c3f5-…-9d1b3f5e7a2c",
"message": "สู้ ๆ นะครับ", "currency": "THB",
"supporter": "แฟนคลับหมายเลข 1", "verification": "verified" },
"event": "tip.confirmed",
"created_at": "2026-09-27T09:31:04.221+00:00",
"schema_version": 1 }- Roses are ฿10 each and there are three, so the amount is 30. Names and messages keep the language the supporter wrote in.
How it is sent, and what happens when it fails
- 1The tip is confirmed
Following the creator's settings. How it was confirmed is in the verification field.
- 2It joins the queue
Each delivery is stored in a queue first, so nothing is lost while your end is down.
- 3POST
It waits up to 5 seconds for an answer and never follows redirects.
- 4A 2xx is success
Any 200 to 299 means received, and the delivery is done.
- 5Anything else is retried
Automatically, for up to 24 hours, with the same id.
Turning the webhook off or changing the destination cancels queued deliveries; nothing more goes to the old URL.
Press เปลี่ยน secret (rotate secret) and queued deliveries are signed with the new secret before they go out.
Deliveries that ran out of time can be sent again with ส่งรายการค้างอีกครั้ง (resend pending) on the same card.
Make sure it really came from Tipjai
Copy the secret from the automations card in the dashboard. On Zapier, use Catch Raw Hook so you get the raw body, then check it in a Code by Zapier step with this same code.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody is the raw text you received, before JSON.parse
export function verifyTipjai(rawBody, headers, secret) {
const ts = headers["x-tipjai-timestamp"];
const got = String(headers["x-tipjai-signature"] || "");
const want = "v1=" + createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && got.length === want.length &&
timingSafeEqual(Buffer.from(got), Buffer.from(want));
}schema_version 1
| Field | Type | Description |
|---|---|---|
id | uuid | This delivery's id, the same value as Idempotency-Key |
event | string | tip.confirmed or tip.test |
created_at | timestamp | When the tip was confirmed |
schema_version | number | Currently 1 |
data.tip_id | uuid | The tip's id |
data.supporter | string | The sender's name; ผู้สนับสนุน (supporter) when they left it blank |
data.amount | number | Amount |
data.currency | string | Currency, normally THB |
data.message | string | The sender's message, empty when there is none |
data.verification | string | How it was confirmed, such as verified or creator_confirmed |
data.gift | object | null | id, name, qty when the tip came with a gift |
tip.confirmedSent every time a tip is confirmed
tip.testSent by ส่งทดสอบ (send a test) in the dashboard, with amount 0 and verification test
