StrimzStrimz

Webhooks

Every event Strimz fires and how to verify signatures.

Signature

Every webhook is signed with HMAC-SHA256 and sent as the Strimz-Signature header:

Strimz-Signature: t=1714003200,v1=ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

t is the Unix timestamp at signing time. Receivers should:

  1. Re-compute hmac_sha256(secret, "${t}.${rawBody}")
  2. Compare timing-safe to v1
  3. Reject if |now - t| > 300s (replay protection window)

The reference implementation lives in @strimz/sdk → verifyWebhookSignature.

Headers

HeaderDescription
Strimz-Signaturet=<unix>,v1=<hex>
Strimz-Event-Idevt_…, globally unique
Strimz-Event-Typedotted name, e.g. payment.completed
X-Strimz-Delivery-Idwhdl_…, the same on every retry of one delivery

Events

EventFired whendata
payment.createdA payment session is created, through the API or a storefrontthe session
payment.completedAn on-chain payment is confirmed for a session{ session, transaction }
payment.failedA session expires unpaid{ session, reason }
subscription.createdThe indexer sees a new subscription on chainthe subscription
subscription.chargedA periodic charge succeeded{ subscription, charge, transaction }
subscription.charge_failedA charge failed for a payment reason (insufficient funds, allowance, etc.){ subscription, charge }
subscription.recovery_attemptThe recovery agent emailed the customer about a failed charge{ subscription, attemptNumber }
subscription.cancelledYou cancelled through the API or dashboardthe subscription
subscription.lapsedThe 48-hour grace window ran out without a successful chargethe subscription
subscription.reactivatedStrimz restored a subscription its operator key cancelled by mistakethe subscription
refund.createdA refund is createdthe refund
refund.completedThe refund transaction confirmed on chain{ refund, transaction }
refund.failedThe refund failed; failureReason says whythe refund
invoice.createdAn invoice is createdthe invoice
invoice.paidAn invoice's payment confirmed{ invoice, transaction }
invoice.overdueAn hourly cron finds the invoice past its due datethe invoice

A cancel the payer makes from their own wallet updates the subscription but does not fire subscription.cancelled.

The agent.job_* and compliance.* event types belong to features that are coming. They are not sent today.

Retry policy

A delivery that times out (10 seconds) or gets a non-2xx response is retried. There are 5 attempts in total, with waits of 1 minute, 5 minutes, 30 minutes and 2 hours between them, about 2 hours 36 minutes from first to last. After the last attempt:

  • The delivery is marked permanently_failed
  • The merchant is emailed
  • 5 permanent failures within 24 hours on the same endpoint auto-disable it

Replaying a delivery from the dashboard or API resets it to attempt 1 and starts the schedule again.

Durability behind the scenes

A few details about how the delivery pipeline is built. You don't need to know any of this to use webhooks, but it's worth understanding if you're evaluating Strimz for production traffic.

Signing secrets are stored as AES-256-GCM ciphertext in Postgres. A database dump leaks ciphertext, not the key used to sign your deliveries. The plaintext lives in Redis as a hot cache for the delivery worker; that way we don't pay the decrypt cost on every send. If Redis is flushed, the scheduler decrypts the column back into the cache on its next boot and webhook delivery resumes with no action on your side.

Every attempt is persisted to the WebhookDelivery table. You get the response code, the response body (truncated to 4,000 characters), the attempt number, the next-attempt time, and the last error, all visible in your dashboard. Every attempt carries an X-Strimz-Delivery-Id header that stays the same across retries of one delivery, so your endpoint can deduplicate on it.

If your endpoint goes dark for a short while (a few minutes, an hour) the retry schedule covers it. If you're down for the whole window of about two and a half hours, the delivery lands as permanently_failed, and you can replay it from the dashboard once you're back up.

On this page