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:

conceptmeaning
EndpointA destination you own: name + target URL + secret key. Each endpoint gets its own receiver URL.
WebhookOne event received on a receiver URL. Payload is AES-256-GCM encrypted and queued for delivery.
DeliveryOne attempt to POST that webhook to your target URL — status code, duration, response body, all logged.

Quickstart — 3 minutes

  1. 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.
  2. 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).
  3. Point your provider at the receiver URL:
    https://api.tifybe.com/webhook/<YOUR_RECEIVER_PATH>
  4. 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

When Tifybe delivers to your target URL, the provider's original headers travel with the request, plus 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:

  1. Take the current Unix time in seconds → timestamp.
  2. Concatenate {timestamp}.{raw request body} — a literal dot between them, body exactly as sent (no re-serialization).
  3. Compute HMAC-SHA256 over that string with your endpoint secret key, hex-encoded.
  4. Send it as X-Tifybe-Signature together with X-Tifybe-Timestamp.

5-minute replay window

Requests whose timestamp is older than 300 seconds are rejected with 401, even with a valid signature. Signatures are compared in constant time.

Headers reference

Inbound — what you send to a receiver URL:

headerrequirednotes
X-Tifybe-Timestampyes*Unix seconds. Must be within 5 minutes of server time (replay protection).
X-Tifybe-Signatureyes*Hex HMAC-SHA256 of "{timestamp}.{raw body}" using your endpoint secret.
X-Idempotency-KeynoUnique ID per event. Falls back to Stripe-Event-Id, then X-GitHub-Delivery, then a payload hash.
X-Tifybe-EventnoFree-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:

headernotes
X-Tifybe-Trace-IdUnique trace ID; find the same value on the webhook detail page.
Content-Typeapplication/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:

  1. Stripe-Event-Id header, if present
  2. X-GitHub-Delivery header, if present
  3. X-Idempotency-Key header, if present
  4. 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:

attemptwait before next attempt
1 → 21 minute
2 → 35 minutes
3 → 430 minutes
4 → 52 hours
5 → 624 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

Webhooks that failed or died can be re-sent from their detail page (and in bulk from the DLQ). While a webhook is retrying, its detail page shows the attempt count and the next scheduled retry.

Delivery requirements — what your server must do

For a delivery to count as successful, your target URL must:

  • Return any 2xx status200, 202,204 all 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

When retries kick in, events can arrive out of order — an event that failed at 09:00 may be redelivered after one that succeeded at 09:05. Every payload carries its original timestamps; design handlers to be order-independent or re-order using them.

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

If your code throws, times out, or returns nothing, Tifybe delivers the original, untouched payload instead. A broken transform degrades to a pass-through — it never blocks or drops an event.

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

statusmeaning
pendingAccepted and queued; a worker will pick it up within seconds.
deliveredYour target URL returned a 2xx. Done.
retryingLast attempt failed; the next attempt is scheduled per the backoff table.
delivered_fallbackPrimary URL exhausted retries, but the fallback URL accepted it.
deadEvery attempt (and fallback, if set) failed. It sits in the DLQ for manual replay.
duplicateSame idempotency key was seen before. Returned 200, never delivered twice.
filteredSkipped by the endpoint's filter rules. Stored for inspection, never delivered or retried.
limit_exceededMonthly plan quota was already used up. Stored for visibility, not delivered.

Rate & size limits

limitvalueon exceed
Inbound rate5,000 events / minute / account429 Too Many Requests
Payload size5 MB413 Payload Too Large
Monthly quotaper 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-GCM before 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

# macOS / Linux curl -fsSL https://tifybe.com/install.sh | sh
# Windows (PowerShell) irm https://tifybe.com/install.ps1 | iex

Checksum-verified. Or go install github.com/emirhannsarial/tifybe-cli/cmd/tifybe@latest, or grab a binary from the releases page.

run

tifybe listen 8080

Prints a public URL instantly, plus a local web viewer to inspect every event in real time.

Persistent URLs (Pro)

Tired of re-pasting a new URL into Stripe after each restart? Run tifybe login with your API key, then tifybe listen 8080 --subdomain=my-startup to claim a URL that never changes.

Plans & limits

freepro — $19/moenterprise — $99/mo
Endpoints325Unlimited
Webhooks / month10,000500,000Unlimited
Delivery attemptsUp to 310 + per-endpoint override20
Log retention7 days30 days90 days
Fallback URLs
DLQ replay & bulk retryLimited
Persistent CLI subdomains
Filter rules per endpoint31010
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

questionanswer
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

symptomlikely cause & fix
401Signature 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 URLThe endpoint is paused (manually or by the circuit breaker) or was deleted. Re-activate it on the Endpoints page.
413Payload over 5 MB. Send a reference (URL/ID) instead of the full blob — webhooks should be thin notifications.
429Over 5,000 events/min. Batch on the sender side or spread the burst; the limit is per account, per minute.
Webhook stuck in pendingWorkers 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 itCheck the delivery log's response body on the webhook detail page — your server returned 2xx but may have errored after responding.
Events arriving twiceRetries after a timeout can redeliver even if you processed the first one. Deduplicate by X-Tifybe-Trace-Id.
Events out of orderExpected during retries — see Delivery requirements. Re-order by payload timestamps.
CLI: subdomain rejectedPersistent subdomains need a Pro plan and `tifybe login`. Anonymous `req_…` tunnels work without an account.
Test webhook shows duplicateThe 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.