TokenTip
Events

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

TypeScopeMeaning
tip.createdcreatorA tipper started checkout; the tip exists as pending.
tip.heldcreatorPayment captured; the fraud-hold window begins.
tip.settledcreatorHold elapsed — credit granted, key limit raised. Carries balance.
tip.refundedcreatorDisputed while still held; no credit was granted.
tip.disputedcreatorChargeback after settling; granted credit clawed back. Carries balance.
key.rotatedcreatorThe standing key was rotated. The secret is never in an event.
solvency.warningoperatorThe pool went tight or over — committed credit is approaching the drawable balance.
settlement.blockedoperatorA settle was declined by a ceiling; the tip stays held and retries.
tip.velocity_blockedoperatorA tip attempt was rate-limited before reaching Stripe (abuse defense).
stream.laggedbothStream-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?

WebhooksStream
Best forServers that must not miss an event.Dashboards, CLIs, live displays.
DeliveryAt-least-once, retried.While connected only.
NeedsA public HTTPS endpoint.Just an outbound connection.