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. Kelviq retries failed deliveries up to 3 times with a 60-second delay between attempts. A delivery is considered successful when your endpoint returns a 2xx status code.

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 also retries failed deliveries automatically. A manual resend is useful when the endpoint is working again and you want to retry immediately.
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.

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.
The retry window applies only to card payments. Kelviq does not retry a failed non-card renewal; the subscription is cancelled after the failure.

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.

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:

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.

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?