Tifybe Documentation
Everything you need to receive, deliver and debug webhooks with guaranteed reliability — from quickstart to the exact retry schedule.
Introduction
Tifybe sits between webhook providers (Stripe, GitHub, Shopify, your own SaaS) and your server. Providers send events to a unique Tifybe receiver URL; Tifybe stores each event durably (encrypted at rest), then delivers it to your target URL with automatic retries, exponential backoff, fallback URLs and a Dead Letter Queue. If your server is down for an hour — or a day — you lose nothing.
Three concepts cover the entire product:
| concept | meaning |
|---|---|
Endpoint | A destination you own: name + target URL + secret key. Each endpoint gets its own receiver URL. |
Webhook | One event received on a receiver URL. Payload is AES-256-GCM encrypted and queued for delivery. |
Delivery | One attempt to POST that webhook to your target URL — status code, duration, response body, all logged. |
Quickstart — 3 minutes
- Create an endpoint. Go to Endpoints → New Endpoint. Enter a name and your real target URL (your Express/Django/Next.js handler). You get back a receiver URL and a secret key.
- Save the secret key. It is shown exactly once. You need it to sign webhooks you send to Tifybe yourself (not needed when a provider like Stripe posts to you).
- Point your provider at the receiver URL:
https://api.tifybe.com/webhook/<YOUR_RECEIVER_PATH>
- Press Test. Every endpoint has a Test button that pushes a signed event through the full pipeline. Watch it arrive under Webhooks in real time.
Receiving webhooks from providers
Give the receiver URL to Stripe, GitHub, Shopify or any provider exactly where they ask for a "webhook URL". Tifybe accepts the event, encrypts the payload, answers 200 immediately, and takes over delivery to your server.
Each endpoint has a signature mode that says who is allowed to send to it:
- Provider signature — paste your provider's signing secret (e.g. Stripe's
whsec_…) and Tifybe verifies each event with the provider's own scheme. Supported natively: Stripe (Stripe-Signature), GitHub (X-Hub-Signature-256), Shopify (X-Shopify-Hmac-Sha256) and Standard Webhooks / Svix (used by Polar, Clerk and many others). The scheme is auto-detected from the request headers. - Tifybe HMAC — for senders you control; sign with your endpoint secret as shown in the section below.
- None — accept unsigned requests. Your receiver URL contains 128 bits of randomness and duplicates are suppressed, so this is a reasonable default for providers without signatures — but prefer provider mode when a signature exists.
Provider events are deduplicated automatically: Tifybe recognizes Stripe-Event-Id and X-GitHub-Delivery natively — a Stripe retry of the same event will never hit your server twice.
Original headers are forwarded
X-Tifybe-Trace-Id so you can correlate any delivery with the dashboard.Sending your own signed webhooks
If you are a SaaS delivering webhooks to your customers, push events into Tifybe and let it handle retries, backoff and the DLQ. Sign each request with your endpoint's secret key:
const crypto = require('crypto');
const payload = JSON.stringify({ user_id: 123, action: "payment_success" });
const timestamp = Math.floor(Date.now() / 1000).toString();
const secret = process.env.TIFYBE_ENDPOINT_SECRET;
// 1. Sign: HMAC-SHA256 over "{timestamp}.{payload}"
const signature = crypto
.createHmac('sha256', secret)
.update(timestamp + "." + payload)
.digest('hex');
// 2. Send to your Tifybe receiver URL
await fetch('https://api.tifybe.com/webhook/YOUR_RECEIVER_PATH', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tifybe-Timestamp': timestamp,
'X-Tifybe-Signature': signature,
'X-Idempotency-Key': crypto.randomUUID(),
},
body: payload,
});HMAC signatures, exactly
This section describes Tifybe's own scheme— used when an endpoint's signature mode is Tifybe HMAC(senders you control) and for signatures Tifybe adds to outbound deliveries. Provider-mode endpoints are verified with the provider's scheme instead (see the section above). The scheme is deliberately boring and standard:
- Take the current Unix time in seconds →
timestamp. - Concatenate
{timestamp}.{raw request body}— a literal dot between them, body exactly as sent (no re-serialization). - Compute
HMAC-SHA256over that string with your endpoint secret key, hex-encoded. - Send it as
X-Tifybe-Signaturetogether withX-Tifybe-Timestamp.
5-minute replay window
401, even with a valid signature. Signatures are compared in constant time.Headers reference
Inbound — what you send to a receiver URL:
| header | required | notes |
|---|---|---|
X-Tifybe-Timestamp | yes* | Unix seconds. Must be within 5 minutes of server time (replay protection). |
X-Tifybe-Signature | yes* | Hex HMAC-SHA256 of "{timestamp}.{raw body}" using your endpoint secret. |
X-Idempotency-Key | no | Unique ID per event. Falls back to Stripe-Event-Id, then X-GitHub-Delivery, then a payload hash. |
X-Tifybe-Event | no | Free-form event name (e.g. invoice.paid) shown in the dashboard. |
* Signature headers are required unless signature verification is disabled for your deployment.
Outbound — what Tifybe sends to your target URL:
| header | notes |
|---|---|
X-Tifybe-Trace-Id | Unique trace ID; find the same value on the webhook detail page. |
Content-Type | application/json (unless the original request set something else). |
(original headers) | Headers from the original provider request are forwarded to you. |
Idempotency & duplicates
Tifybe picks the deduplication key for each incoming event in this order:
Stripe-Event-Idheader, if presentX-GitHub-Deliveryheader, if presentX-Idempotency-Keyheader, if present- Otherwise: an HMAC hash of the payload, scoped to the endpoint
A repeated key gets a 200 response (so providers stop retrying) but is recorded with status duplicate and never delivered twice. Duplicate records are kept for 7 days.
Retries & backoff schedule
A delivery counts as failed when your server returns a non-2xx, times out, or refuses the connection. Failed deliveries are retried on this exact schedule:
| attempt | wait before next attempt |
|---|---|
| 1 → 2 | 1 minute |
| 2 → 3 | 5 minutes |
| 3 → 4 | 30 minutes |
| 4 → 5 | 2 hours |
| 5 → 6 | 24 hours |
| 6+ | 24 hours each |
Each plan sets the total number of delivery attempts (the first delivery plus automatic retries): up to 3 on Free, 10 on Pro, 20 on Enterprise. On Pro and above you can also set a lower per-endpoint override for endpoints where staleness matters more than persistence.
Manual replays
Delivery requirements — what your server must do
For a delivery to count as successful, your target URL must:
- Return any 2xx status —
200,202,204all count. 3xx redirects are not followed; 4xx/5xx count as failures. - Respond within the timeout — connection must open within 5 seconds; the response timeout is configurable per endpoint (1–60 s). Do heavy work after responding: acknowledge first, process async.
- Be idempotent — retries mean the same event can arrive more than once. Use
X-Tifybe-Trace-Id(or your own event IDs) to deduplicate on your side.
Ordering is not guaranteed
Responses are logged per attempt (status code, duration, first 1 KB of the body) — see any webhook's detail page for the full timeline.
Filters & transformations
Both are configured per endpoint (endpoint form → Filters & transform) and run inside Tifybe — your server only ever sees the events you actually want, in the shape you want.
Filter rules decide whether an event is delivered. Each rule tests a payload field (dot-path like data.amount, array indices like items.0.id) or a request header via the headers. prefix. Operators: equals, not_equals, contains, starts_with, ends_with, gt, lt, exists, not_exists. Combine rules with all (AND) or any (OR). Non-matching events are stored with status filtered— inspectable in the dashboard, never delivered, never retried, so they don't burn delivery attempts on your target. Free plans get 3 rules per endpoint, paid plans 10.
Payload transformations (Pro+) rewrite the payload beforedelivery with a JavaScript function — rename fields, drop noise, reshape a provider's format into your API's:
function transform(event) {
// event.payload = parsed JSON body
// event.headers = original request headers
return {
kind: event.payload.type,
amount_cents: event.payload.data.amount,
source: event.headers["User-Agent"],
};
}- The return value becomes the delivered body — objects are JSON-encoded, strings sent as-is.
- Runs in a sandbox (no network, no filesystem) with a 100 ms execution limit and a 10 KB code limit.
- Applied on every attempt, including automatic retries and manual DLQ replays.
Transforms can never lose your data
Fallback URLs & circuit breaker
Fallback URL (Pro+): when the primary target exhausts all retries, Tifybe makes one final attempt to your fallback URL — e.g. a backup Lambda or a logging service. If it succeeds, the webhook is marked delivered_fallback; otherwise it goes to the DLQ.
Circuit breaker: if an endpoint accumulates 50 consecutive dead webhooks, Tifybe automatically pauses it and emails you. This protects your monthly quota from burning while your server is down. Fix the target, flip the endpoint back to active, then bulk-retry from the DLQ. Any successful delivery resets the counter.
Dead Letter Queue
Webhooks that exhaust every attempt land in the Dead Letter Queue with their full payload and delivery history intact. Nothing is deleted. From there you can:
- Retry one — re-queues it through the normal pipeline.
- Retry all — bulk replay after an outage (Pro+; Free has a cooldown).
- Delete — permanently removes the webhook and its history.
You'll also get an email alert based on your failure threshold — e.g. "alert me after 5 consecutive failures".
Webhook statuses
| status | meaning |
|---|---|
| pending | Accepted and queued; a worker will pick it up within seconds. |
| delivered | Your target URL returned a 2xx. Done. |
| retrying | Last attempt failed; the next attempt is scheduled per the backoff table. |
| delivered_fallback | Primary URL exhausted retries, but the fallback URL accepted it. |
| dead | Every attempt (and fallback, if set) failed. It sits in the DLQ for manual replay. |
| duplicate | Same idempotency key was seen before. Returned 200, never delivered twice. |
| filtered | Skipped by the endpoint's filter rules. Stored for inspection, never delivered or retried. |
| limit_exceeded | Monthly plan quota was already used up. Stored for visibility, not delivered. |
Rate & size limits
| limit | value | on exceed |
|---|---|---|
| Inbound rate | 5,000 events / minute / account | 429 Too Many Requests |
| Payload size | 5 MB | 413 Payload Too Large |
| Monthly quota | per plan (10k / 500k / ∞) | stored as limit_exceeded, not delivered |
Quota-exceeded events are still recorded so you can see exactly what you missed — upgrade and the next events flow immediately.
Security model
- Encryption at rest: every payload and endpoint secret is encrypted with
AES-256-GCMbefore touching the database. - Transport: all traffic is TLS. Receiver paths are 64-char random hex — unguessable.
- Authentication: dashboard sessions use Clerk (JWT). Server-to-server calls use API keys (
tfy_…) stored only as bcrypt hashes. - Replay protection: 5-minute timestamp window + constant-time signature comparison.
- Data lifecycle: logs expire per plan retention; account deletion soft-deletes immediately and purges after 30 days.
CLI — receive webhooks on localhost
The open-source Tifybe CLI opens a secure WebSocket tunnel so webhooks reach your localhost during development — no port forwarding, no third-party tunnels.
install
Checksum-verified. Or go install github.com/emirhannsarial/tifybe-cli/cmd/tifybe@latest, or grab a binary from the releases page.
run
Prints a public URL instantly, plus a local web viewer to inspect every event in real time.
Persistent URLs (Pro)
tifybe login with your API key, then tifybe listen 8080 --subdomain=my-startup to claim a URL that never changes.Plans & limits
| free | pro — $19/mo | enterprise — $99/mo | |
|---|---|---|---|
| Endpoints | 3 | 25 | Unlimited |
| Webhooks / month | 10,000 | 500,000 | Unlimited |
| Delivery attempts | Up to 3 | 10 + per-endpoint override | 20 |
| Log retention | 7 days | 30 days | 90 days |
| Fallback URLs | — | ✓ | ✓ |
| DLQ replay & bulk retry | Limited | ✓ | ✓ |
| Persistent CLI subdomains | — | ✓ | ✓ |
| Filter rules per endpoint | 3 | 10 | 10 |
| Payload transformations (JS) | — | ✓ | ✓ |
Checkout and invoicing are handled by Polar (our merchant of record). Cancel anytime — access lasts until the period ends. Manage billing →
Billing & subscriptions
| question | answer |
|---|---|
| How does renewal work? | Paid plans renew automatically every month. Your usage counter resets at each billing cycle (Free plans reset on the 1st of the month). |
| How do I cancel? | Billing → Manage subscription → Cancel. No refund is triggered — you keep full access until the period ends, then drop to Free automatically. Nothing else to do. |
| Can I get a refund? | Yes — contact us through the form in Settings. Eligible refunds are processed via Polar back to your original payment method, and your plan reverts to Free immediately. |
| What happens if my payment fails? | We email you right away. Update your card in the customer portal; if the invoice stays unpaid, the account drops to Free (your data and endpoints are kept). |
| Who is the seller on my invoice? | Polar Software Inc. — our merchant of record. Polar stores your card (it never touches our servers) and handles VAT/sales tax. |
| What happens to my data on downgrade? | Nothing is deleted. Endpoints over the Free limit stay but you can't create new ones; log retention shortens to 7 days going forward. |
Troubleshooting
| symptom | likely cause & fix |
|---|---|
401 | Signature or timestamp rejected. Check: HMAC over "{timestamp}.{raw body}" (literal dot, exact bytes), hex encoding, and clock skew under 5 minutes (sync NTP). |
404 on receiver URL | The endpoint is paused (manually or by the circuit breaker) or was deleted. Re-activate it on the Endpoints page. |
413 | Payload over 5 MB. Send a reference (URL/ID) instead of the full blob — webhooks should be thin notifications. |
429 | Over 5,000 events/min. Batch on the sender side or spread the burst; the limit is per account, per minute. |
| Webhook stuck in pending | Workers pick jobs up within seconds normally. If it persists, check the status page — or the queue may be draining after a burst. |
| Delivered but my app didn't process it | Check the delivery log's response body on the webhook detail page — your server returned 2xx but may have errored after responding. |
| Events arriving twice | Retries after a timeout can redeliver even if you processed the first one. Deduplicate by X-Tifybe-Trace-Id. |
| Events out of order | Expected during retries — see Delivery requirements. Re-order by payload timestamps. |
| CLI: subdomain rejected | Persistent subdomains need a Pro plan and `tifybe login`. Anonymous `req_…` tunnels work without an account. |
| Test webhook shows duplicate | The Test button injects unique IDs to bypass idempotency — if you see duplicate, you re-sent an identical custom payload; change X-Idempotency-Key. |
Still stuck? Every webhook's detail page shows the exact request we received and every delivery attempt with response codes and bodies — start there, then write to us with the trace ID.