Setting Up an Endpoint
- Go to Developers → Webhooks in your Crovver dashboard
- Click + Add Endpoint
- Enter your HTTPS URL — e.g.
https://api.your-app.com/webhooks/crovver - Copy the signing secret shown — it is displayed only once and cannot be retrieved again
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.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_endis in the future but within 7 days- Status is
active,trial,pending_cancelorpast_due - Billing is
manual— or the status ispending_cancel(a provider-billed subscription that was already told to stop) - The plan is not free
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.
Credit Balances
Every event that names a subscription carries acredits array with the tenant’s per-pool
balances, so you can keep your own mirror in sync without an extra API call.
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.- Early renewal
- On-time or late renewal
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 anX-Crovver-Signature header. Always verify it before processing the event.
HMAC-SHA256 of the raw request body using your endpoint’s signing secret.
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
id — so your server can safely deduplicate.
Testing Locally
Use the Send Test button in your dashboard to fire awebhook.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:
https://abc123.ngrok-free.app/webhooks/crovver — then use Send Test to verify your handler end-to-end before deploying.