Skip to main content
Kelviq can push event notifications to your server whenever something meaningful happens — a subscription is created, an invoice is paid, or a refund is issued. This guide explains how to register an endpoint, verify incoming requests, and handle events.

How It Works

When an event occurs, Kelviq creates a WebhookEvent and fans it out to all enabled endpoints you have registered for that event type. Each delivery is a single POST request with a JSON body and signed headers. A delivery is considered successful when your endpoint returns a 2xx status code. Kelviq retries a delivery up to 3 times, with a 60-second delay between attempts, when the request fails to connect, times out after 10 seconds, or your endpoint returns a 5xx status code or 429 Too Many Requests. Other 4xx and 3xx responses are not retried; Kelviq does not follow redirects.

Registering an Endpoint

Go to Settings → Webhooks in the Kelviq dashboard to create a webhook endpoint. You will specify:
  • URL — The public HTTPS URL Kelviq will POST events to.
  • Events — The subset of event types you want to subscribe to.
After creation, Kelviq generates a signing secret for the endpoint in the format kq_whsec_<random>. Store this secret securely — you will need it to verify signatures.
Your signing secret is shown only once. If you lose it, you must regenerate it from the dashboard.

Inspect and resend deliveries

Open Settings → Webhooks and select an endpoint to view its delivery logs. Each delivery shows the event type, status, response code, and response body. To retry a delivery:
  1. Fix the endpoint or deployment issue that caused the delivery to fail.
  2. Open the failed delivery in the webhook logs.
  3. Click Resend.
  4. Review the new delivery attempt and its response.
Kelviq automatically retries timeouts, connection failures, and 5xx or 429 responses, but not other 4xx or 3xx responses. Use a manual resend to redeliver those after you fix the endpoint, or to retry immediately instead of waiting for the next automatic attempt.
Automatic retries and manual resends can deliver the same event more than once. Process webhooks idempotently using the event id or the webhook-id header.

Event Types


Checkout, subscription, and payment lifecycle

Checkout and subscription creation

checkout.completed confirms that checkout finished successfully, but it does not include subscription_id. Kelviq creates the subscription after checkout and sends its ID in subscription.created. Use your customer identifier or checkout metadata to correlate the two events. See Add metadata to checkout for request and webhook examples. Do not treat the checkout redirect as proof that a subscription has been created.

Custom field responses

When a checkout session collects custom fields, the customer’s responses are included as custom_fields_data in data.object of checkout.completed and of the order.created, order.updated, and order.refunded events for the resulting order. The object is keyed by custom field identifier and is empty ({}) when the session collected no custom fields.

Successful invoice payments

Kelviq sends invoice.paid whenever an invoice is paid, including invoices that are created directly with a PAID status. Use this event for payment-driven work such as recording revenue, starting an asynchronous provisioning job, or notifying an internal system. For request-time access checks, query the customer’s current entitlements instead of relying on a local webhook-derived subscription state.

Failed invoice payments

Kelviq sends invoice.payment_failed when a subscription invoice payment fails or requires customer action. Use data.object.subscription_id and the embedded customer to identify the affected account. The invoice status is PAYMENT_FAILED for a failed attempt and can remain OPEN when the payment requires customer action. This event can be followed by invoice.paid if the customer completes authentication or a later retry succeeds. Handle both events idempotently and treat the latest invoice and subscription state as authoritative.

Subscription end dates

Subscription webhook objects include end_date inside data.object. Kelviq sets the field when a subscription has been cancelled or is scheduled to cancel on a future date. Otherwise, the value is null. Use end_date for the subscription’s scheduled or final end. billing_period_end_time describes the current billing period instead and should not be treated as the subscription’s cancellation date.

Failed renewal payments

Kelviq does not currently send a separate payment.failed event. For a failed card renewal:
  1. The subscription moves to past_due.
  2. Kelviq emails the customer a payment link.
  3. Kelviq sends subscription.updated with the new subscription state.
  4. Kelviq retries collection up to eight times over 14 days.
  5. If all recovery attempts fail, Kelviq cancels the subscription and sends subscription.cancelled.
Do not remove access when you receive the first subscription.updated with past_due unless that is your own policy. Use the final subscription status or a live entitlement check to decide when access should end.
Card and UPI AutoPay renewals are retried for up to 14 days. If the payment still fails, the subscription is canceled.

Billing period field names

Subscription payloads use billing_period_start_time and billing_period_end_time. Match these names exactly when parsing dates. They describe the current billing period; use end_date for the subscription’s scheduled or final end.

Plan changes

By default, Kelviq applies a plan change immediately, charges any prorated difference, and sends subscription.plan_changed. After the generated invoice is paid, Kelviq sends the related order and invoice events. If you update a subscription with paymentBehavior: "activate_on_payment", the existing subscription remains active while payment is pending. A payment method that requires customer action leaves the updated subscription in incomplete. After payment succeeds, the updated subscription becomes active and the previous subscription becomes superseded. If an immediate charge fails, or the customer does not complete payment before expiry, the updated subscription becomes incomplete_expired and the existing subscription remains active. Do not provision the new plan from the update API response alone. Read the latest subscription status or query the customer’s current entitlements before changing access.

Pauses and resumes

A pause sends subscription.paused with status: "paused", paused_at, and pause_resumes_at, which is null for an open-ended pause. Treat it like a cancellation for access: entitlement checks already return false for a paused customer, so clear any cached result when the event arrives (client.entitlements.invalidateCache in Node, client.entitlements.invalidate_cache in Python). The subscription is quiet until it resumes. Renewals that fall inside the pause are skipped without subscription.updated or invoice events. subscription.resumed arrives when billing starts again, at the scheduled date or after a resume call, with status: "active" and a new billing period. Usage counters are reset at that point. Both events fire whichever surface made the change: the dashboard, the customer portal, or your API call.

Partial and full refunds

order.refunded identifies whether the order is partially or fully refunded through its status:
  • PARTIAL_REFUND for a partial refund
  • REFUNDED for a full refund
The order.refunded payload does not include the refunded amount. Read amount or amount_units from refund.created and refund.updated when you need the exact value.

Request Headers

Every webhook request from Kelviq includes the following headers:

Payload Structure

The request body is a JSON object with the following top-level fields:
data.object uses the same subscription payload shape as subscription.created/subscription.updated; the trial’s remaining time is in trial_end. Kelviq sends this once, a few days before trial_end. It’s skipped if the subscription has already converted to active or has no trial days left.
attempt_count, next_payment_attempt, and payment_link are also included on invoice.created and invoice.paid payloads. failure_details (with code, message, and payment_method_type) describes the most recent failed attempt and is null once the invoice moves past that attempt (e.g. a later invoice.paid for the same invoice). payment_link is null whenever the invoice isn’t currently payable (e.g. already paid).

Verifying Signatures

Always verify the signature before processing a webhook. Skipping verification exposes your endpoint to spoofed requests from third parties.
Kelviq signs every request using HMAC-SHA256. To verify a request:
  1. Read the webhook-id and webhook-timestamp headers.
  2. Construct the signed string by concatenating: {webhook-id}.{webhook-timestamp}.{raw-request-body} (joined with .)
  3. Compute HMAC-SHA256 over the signed string using your endpoint’s signing secret as the key.
  4. Compare the hex digest to the signature in the webhook-signature header (strip the v1, prefix before comparing).
  5. Reject the request if the signatures do not match.
The raw request body must be used exactly as received — before any JSON parsing. The compact JSON serialization (no spaces) is what Kelviq sends and signs.
Optionally, also check that webhook-timestamp is within a few minutes of your server’s current time to defend against replay attacks.

Code Examples

Use express.raw() (not express.json()) in Node.js so that req.body contains the unmodified request bytes. Parsing the body before verification will break the signature check. On a framework built on the Fetch API (Next.js route handlers, Hono, Remix, Cloudflare Workers), read the body with await request.text() and take each header with request.headers.get(...), since request.headers is a Headers object rather than a plain object.

Best Practices

  • Return 2xx fast. Acknowledge the webhook immediately and process it asynchronously. Long-running handlers increase the risk of timeouts and duplicate retries.
  • Make handlers idempotent. The same event may be delivered more than once. Use the top-level event id or the webhook-id header as the deduplication key.
  • Validate the timestamp. Reject requests where webhook-timestamp is more than 5 minutes from your server’s clock to prevent replay attacks.
  • Use hmac.compare_digest / hmac.Equal / crypto.timingSafeEqual. Constant-time comparison prevents timing side-channel attacks.
  • Store the raw body before parsing. Signature verification operates on the raw bytes, not the deserialized object.

Need Help?