Skip to main content
Crovver can push subscription lifecycle events to HTTP endpoints you register. When a subscription is created, activated, renewed, or canceled — whether triggered manually from your dashboard or by a payment provider — Crovver signs a JSON payload and POSTs it to every active endpoint you’ve configured.

Setting Up an Endpoint

  1. Go to Developers → Webhooks in your Crovver dashboard
  2. Click + Add Endpoint
  3. Enter your HTTPS URL — e.g. https://api.your-app.com/webhooks/crovver
  4. Copy the signing secret shown — it is displayed only once and cannot be retrieved again
Your endpoint must respond with a 2xx status within 5 seconds. Any other response or a timeout is recorded as a failed delivery.

Event Envelope

Every event shares the same top-level structure:
data.external_tenant_id is the ID you provided when creating the tenant — use this to identify the user or workspace in your own system. data.tenant_name is the human-readable name for quick identification.
Every event that names a subscription also carries the plan and product it belongs to, so you never have to resolve a UUID to route the event:
If you sell more than one product, product_slug is what disambiguates an event. Two products can define the same credit pool_key, so a credits.refilled naming only the pool is not enough on its own to decide which product’s balance moved.

Events Reference

When a free trial ends and the first charge succeeds, both subscription.updated (status: trialing → active) and subscription.renewed (payment received) fire in sequence. These represent distinct state changes — handle them independently and use the top-level id to deduplicate if needed.
Redirect-gateway checkouts (eSewa, Khalti, ConnectIPS) verify the payment server-side after the browser returns, then emit subscription.activated with the tenant’s freshly granted credits. If you integrated against these gateways before, note that this event is newly emitted — previously an activation through them produced no webhook at all.

Expiry Warnings

subscription.expiring is an early warning, not a state change: nothing has happened to the subscription yet. It fires for subscriptions that will lapse unless someone acts — the ones that do not auto-renew. A subscription is selected when all of these hold:
  • current_period_end is in the future but within 7 days
  • Status is active, trial, pending_cancel or past_due
  • Billing is manualor the status is pending_cancel (a provider-billed subscription that was already told to stop)
  • The plan is not free
Provider-billed subscriptions are otherwise excluded, because Stripe renews them on its own.
days_until_expiry is the real remaining days, so it varies run to run. milestone labels which warning this is; today expiring is the only value, reserved so additional milestones (T-3, T-1) can be added later as deliberate extra sends.
Sent once per subscription per period. The dedupe key is (subscription, period end, channel), so a subscription warned on the first daily run that sees it is never warned again for that period — even though the 7-day window keeps matching it. Renewing and reaching a new period end makes it eligible again.
The credits block tells you what balance is about to lapse alongside the period, which is what makes this event useful in a renewal nudge rather than just a date reminder.

Credit Balances

Every event that names a subscription carries a credits array with the tenant’s per-pool balances, so you can keep your own mirror in sync without an extra API call.
qty_granted is cumulative, so it will usually be much larger than qty_remaining and the two are not meant to reconcile against each other. A pool granting 500 per window shows qty_granted: 1750 and qty_remaining: 500 in its fourth window — 1,750 handed out across the contract’s history, 500 available today. To show “500 of 500 left” in your UI, compare qty_remaining against the plan’s per-period limit, not against qty_granted.
Because qty_remaining is a full snapshot rather than a delta, set your stored balance from it rather than incrementing. A receiver that sets self-corrects after a missed delivery. On addon.purchased, the top-level qty_granted is that one purchase’s delta (“this purchase added 500”), while qty_granted inside credits is the running total.

credits.refilled

Recurring subscriptions refill when the provider charges them, and that is already covered by subscription.renewed. But a manually billed contract can be paid for several credit windows up front — a monthly plan prepaid for a year — and those later windows open with no payment and no status change. Nothing else would tell you the credits arrived. credits.refilled is that signal. It fires from a scheduled job at each window boundary:
billing_period is the window ordinal within the contract — window 4 of a prepaid year. current_period_start and current_period_end are the contract bounds, not the window’s; a refill never moves them. It is not sent when the plan defines no credit pools, when a concurrent run already advanced that window, or for provider-billed subscriptions.

Early renewals refill later, not at payment

This is the case most integrations get wrong, so handle it explicitly.
Paid before the current period ends. Crovver extends current_period_end and keeps the original period start, so the contract now spans more credit windows.No credits are granted at payment time. The subscription.renewed you receive carries pre-refill balances — accurate at that instant, but not the post-renewal figure.Each newly paid-for window is granted later, at its real boundary, and arrives as credits.refilled. Sync balances from that event, not from the renewal.
Webhooks cover every grant, but not consumption — your backend drives that, and POST /api/public/credits/consume returns the new remaining in its response. Write through from there, set from the credits block on events, and read credit balance when you need certainty.

Verifying Signatures

Every request includes an X-Crovver-Signature header. Always verify it before processing the event.
The signature is HMAC-SHA256 of the raw request body using your endpoint’s signing secret.
Always verify the signature against the raw request body bytes — not a re-serialized version. JSON parsers may reorder keys, which will break the HMAC comparison.

Delivery & Retries

Crovver delivers each event once. If your endpoint is down or returns a non-2xx status, the delivery is recorded as failed. In Developers → Webhooks, click View Deliveries on any endpoint to see:
  • The full JSON payload that was sent
  • The HTTP status and response body your server returned
  • Attempt count and timestamp
Click Retry on any failed delivery to re-fire it with the same id — so your server can safely deduplicate.
Use the top-level id field as an idempotency key. Store processed event IDs and skip duplicates to handle retries safely.

Testing Locally

Use the Send Test button in your dashboard to fire a webhook.test event to any registered endpoint. The delivery and its payload will appear in the View Deliveries log immediately. To receive events on your local machine, expose it with a tunneling tool like ngrok:
Register the generated HTTPS URL as your endpoint — e.g. https://abc123.ngrok-free.app/webhooks/crovver — then use Send Test to verify your handler end-to-end before deploying.

Security Notes