Webhooks API
Technical reference for the Stripe webhook integration that automatically manages licenses based on subscription events.
Overview
Nareli uses Stripe webhooks to automatically manage license lifecycle events. When a customer subscribes, upgrades, downgrades, cancels, or has a payment issue, Stripe sends a webhook event to the Nareli server. The server processes these events and updates the corresponding license in the database. This ensures that license status always reflects the current subscription state without requiring any manual intervention.
Webhook Endpoint
The Stripe webhook endpoint is located at POST /api/webhooks/stripe. This endpoint accepts raw request bodies with the Stripe event payload and requires a valid Stripe signature header for verification. The endpoint always returns a 200 status with { "received": true } on success, or a 400 status if the signature is invalid or the stripe-signature header is missing.
POST /api/webhooks/stripe
Content-Type: application/json
stripe-signature: t=1741564800,v1=abc123...
{ Stripe event payload }Handled Events
The webhook handler processes five Stripe event types. checkout.session.completed fires when a customer completes the checkout flow and a new subscription is created. customer.subscription.updated fires when a subscription is modified (plan change, status change). customer.subscription.deleted fires when a subscription is canceled and the billing period ends. invoice.payment_failed fires when a recurring payment attempt fails. invoice.paid fires when a payment succeeds (including retries after a failure).
checkout.session.completed
When a customer completes checkout, the handler retrieves the subscription details from Stripe, determines the plan type from the price ID, and finds the organization by Stripe customer ID. If the organization already has a license, it is updated with the new plan type, subscription ID, price ID, and feature flags. If no license exists yet, a new one is generated with a fresh license key in the NRLI-XXXX-XXXX-XXXX-XXXX format. The license status is set to "active".
// What happens internally:
// 1. Retrieve subscription from Stripe
// 2. Map priceId -> plan type ("pro" or "business")
// 3. Find organization by stripe_customer_id
// 4. Create or update the license record
// 5. Set features based on plan type
// New license key generated:
// NRLI-A3F1-8B2C-D4E7-9F06customer.subscription.updated
This event handles plan changes and subscription status updates. The handler maps the new price ID to a plan type, updates the license's type, features, and price per seat. It also checks the Stripe subscription status: if the status is "past_due" or "unpaid", the license status is set to "suspended". Otherwise, it remains "active". This ensures that users who upgrade or downgrade see their feature access change automatically.
Plan upgrades take effect immediately. Downgrades are typically scheduled by Stripe to take effect at the end of the billing period.
customer.subscription.deleted
When a subscription ends (either from cancellation or non-payment), the handler downgrades the license to the Free plan. It sets the type to "free", clears the Stripe subscription and price IDs, sets the price per seat to 0, and updates the feature flags to the Free tier defaults (only basic time tracking enabled). The license key itself remains unchanged so the user does not need to re-enter it.
// After subscription.deleted, the license becomes:
{
"type": "free",
"features": {
"timeTracking": true,
"clients": false,
"projects": false,
"tasks": false,
"ai": false,
"slack": false,
"reports": false,
"recurring": false,
"goals": false
},
"stripeSubscriptionId": null,
"stripePriceId": null,
"pricePerSeat": 0
}invoice.payment_failed
When a payment fails, the handler finds the license associated with the subscription and sets its status to "suspended". This temporarily restricts access to paid features in the desktop app. The user receives an email notification from Stripe about the failed payment and can update their payment method in the billing portal. Stripe will automatically retry the charge according to its smart retry schedule.
invoice.paid
When a payment succeeds (including after a retry following a failure), the handler reactivates the license by setting its status back to "active". This is particularly important for recovering from suspended states — once the overdue payment goes through, the user regains full access to their plan's features without any manual intervention.
Webhook Signature Verification
Every incoming webhook request is verified using Stripe's signature verification mechanism. The server uses the STRIPE_WEBHOOK_SECRET environment variable to validate the stripe-signature header. This ensures that webhook events genuinely originate from Stripe and have not been tampered with. If verification fails, the endpoint returns a 400 error and the event is not processed. This prevents attackers from forging webhook events to manipulate licenses.
// Signature verification (handled automatically):
const event = stripe.webhooks.constructEvent(
requestBody,
stripeSignatureHeader,
process.env.STRIPE_WEBHOOK_SECRET
);
// If verification fails, a 400 is returned:
// { "error": "Webhook signature verification failed: ..." }Setting Up Webhooks in Stripe
To configure webhooks in your Stripe dashboard, go to Developers > Webhooks and click "Add endpoint". Set the endpoint URL to https://nareli.app/api/webhooks/stripe. Select the following events to listen for: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed, and invoice.paid. After creating the endpoint, copy the signing secret and set it as the STRIPE_WEBHOOK_SECRET environment variable on your server.
Only select the five events listed above. Subscribing to unnecessary events increases load without providing any benefit.
Testing with Stripe CLI
During development, you can use the Stripe CLI to forward webhook events to your local server. Install the Stripe CLI, log in with your Stripe account, and run the forward command. This creates a temporary tunnel from Stripe to your local development server. You can also trigger specific test events to verify your webhook handler works correctly.
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Log in
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger a test event
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failedRelated Documentation
On this page