The tip lifecycle, as it happens.
Two delivery modes, one payload. Register a webhook and we POST each event to you, signed; or open the stream and read them live. Both carry the same envelope, so you write the handler once.
The envelope
Every event shares this shape. data is fixed per type, so
you can switch on the type and trust the payload.
{
"id": "evt_1a2b3c4d",
"type": "tip.settled",
"created": "2026-07-19T10:07:00Z",
"scope": "creator",
"data": { "tip": { … }, "balance": "4.068225" }
}
Dedupe on id. TokenTip may redeliver an event; treating
delivery as at-least-once is the only safe assumption.
Event catalog
| Type | Scope | Meaning |
|---|---|---|
| tip.created | creator | A tipper started checkout; the tip exists as pending. |
| tip.held | creator | Payment captured; the fraud-hold window begins. |
| tip.settled | creator | Hold elapsed — credit granted, key limit raised. Carries balance. |
| tip.refunded | creator | Disputed while still held; no credit was granted. |
| tip.disputed | creator | Chargeback after settling; granted credit clawed back. Carries balance. |
| key.rotated | creator | The standing key was rotated. The secret is never in an event. |
| solvency.warning | operator | The pool went tight or over — committed credit is approaching the drawable balance. |
| settlement.blocked | operator | A settle was declined by a ceiling; the tip stays held and retries. |
| tip.velocity_blocked | operator | A tip attempt was rate-limited before reaching Stripe (abuse defense). |
| stream.lagged | both | Stream-only. You fell behind the buffer and missed n events — reconcile with GET /v1/tips. |
Webhooks
Register an endpoint and store the returned secret — it is shown once:
Endpoints must be https and must resolve to a
publicly routable address. URLs pointing at loopback, private, link-local, or
otherwise reserved space are rejected with 400, at registration
and again at delivery; redirects are not followed.
curl -X POST -H "Authorization: Bearer tt_your_token" \
-H "Content-Type: application/json" \
-d '{"url":"https://you.example/hook","events":["tip.settled"]}' \
https://tokentip.to/v1/webhooks
Verifying signatures
Each delivery carries a signature header:
X-TokenTip-Signature: t=1770000000,v1=6f2c…
v1 is HMAC-SHA256(secret, "{t}.{raw_body}"), hex-encoded —
the same scheme Stripe uses, and the same one we use to verify Stripe's inbound
hooks. Compare in constant time and reject timestamps outside a tolerance
(300 s) so a captured delivery cannot be replayed. Verify against the raw
body, before any JSON parsing.
from tokentip import webhooks
event = webhooks.verify(
raw_body, # bytes or str, unparsed
request.headers["X-TokenTip-Signature"],
signing_secret,
)
# raises SignatureVerificationError on a bad signature or stale timestamp
ev, err := tokentip.VerifyWebhook(
body,
r.Header.Get("X-TokenTip-Signature"),
signingSecret,
tokentip.DefaultTolerance,
)
if err != nil {
http.Error(w, "bad signature", http.StatusBadRequest)
return
}
import { webhooks } from "@copyleftdev/tokentip";
const event = await webhooks.verify(
rawBody,
request.headers["x-tokentip-signature"],
signingSecret,
);
Respond 2xx promptly; slow handlers should acknowledge first and work
afterwards. Anything else counts as a failure and is retried after
30s, 2m, 10m, 1h, then 6h. After six failed attempts (~8 hours) the
delivery is marked failed and no longer retried. Deliveries are persisted before the
first attempt, so they survive a restart.
The live stream
Open /v1/events (or /v1/operator/events with an operator
token) and read text/event-stream. The connection stays open, with
keep-alives, until you disconnect.
curl -N -H "Authorization: Bearer tt_your_token" https://tokentip.to/v1/events
with TokenTip("tt_your_token") as tt:
for event in tt.events():
if event.type == "tip.settled":
print("credit granted:", event.tip.net_to_tokens, "→", event.balance)
stream, err := tt.StreamEvents(ctx, false)
defer stream.Close()
for {
ev, err := stream.Next()
if err == io.EOF {
break
}
fmt.Println(ev.Type, ev.Tip().NetToTokens)
}
for await (const event of tt.events()) {
if (event.type === "tip.settled") {
console.log("settled", event.data);
}
}
Which should I use?
| Webhooks | Stream | |
|---|---|---|
| Best for | Servers that must not miss an event. | Dashboards, CLIs, live displays. |
| Delivery | At-least-once, retried. | While connected only. |
| Needs | A public HTTPS endpoint. | Just an outbound connection. |