Skip to main content

Overview

Tresa Contafy uses Stripe webhooks to notify your application about subscription lifecycle events in real-time. Webhooks are HTTP callbacks that Stripe sends to your server when events occur.
Webhooks are required for proper subscription management. They ensure your application stays synchronized with Stripe’s payment and subscription state.

Webhook Events

The API handles the following Stripe webhook events:

checkout.session.completed

Fired when a user completes the Stripe checkout process. Actions:
  • Creates or updates subscription in database
  • Extracts plan and billing information
  • Records customer and subscription IDs
  • Marks trial as used if applicable
  • Records promotion code redemption
{
  "type": "checkout.session.completed",
  "data": {
    "object": {
      "id": "cs_test_xxxxxxxxxxxxx",
      "mode": "subscription",
      "customer": "cus_XXXXXXXXXX",
      "subscription": "sub_XXXXXXXXXX",
      "metadata": {
        "userId": "123e4567-e89b-12d3-a456-426614174000",
        "plan": "PRO",
        "billing": "annual"
      }
    }
  }
}

customer.subscription.updated

Fired when a subscription changes (upgrade, downgrade, renewal). Actions:
  • Updates subscription plan and price
  • Updates billing period dates
  • Updates cancellation status
  • Records plan changes
{
  "type": "customer.subscription.updated",
  "data": {
    "object": {
      "id": "sub_XXXXXXXXXX",
      "customer": "cus_XXXXXXXXXX",
      "status": "active",
      "current_period_start": 1709251200,
      "current_period_end": 1711929600,
      "cancel_at_period_end": false
    }
  }
}

customer.subscription.deleted

Fired when a subscription is cancelled or expires. Actions:
  • Marks subscription as CANCELLED
  • Creates automatic FREE subscription
  • Records cancellation date
  • Downgrades user to FREE plan
{
  "type": "customer.subscription.deleted",
  "data": {
    "object": {
      "id": "sub_XXXXXXXXXX",
      "customer": "cus_XXXXXXXXXX",
      "status": "canceled",
      "canceled_at": 1709251200
    }
  }
}

invoice.payment_succeeded

Fired when a subscription renewal payment succeeds. Actions:
  • Updates subscription status to ACTIVE
  • Updates billing period dates
  • Records successful payment
{
  "type": "invoice.payment_succeeded",
  "data": {
    "object": {
      "id": "in_xxxxxxxxxxxxx",
      "subscription": "sub_XXXXXXXXXX",
      "amount_paid": 79900,
      "status": "paid"
    }
  }
}

invoice.payment_failed

Fired when a subscription renewal payment fails. Actions:
  • Updates subscription status to PAST_DUE
  • Notifies user of payment failure
  • May restrict features depending on configuration
{
  "type": "invoice.payment_failed",
  "data": {
    "object": {
      "id": "in_xxxxxxxxxxxxx",
      "subscription": "sub_XXXXXXXXXX",
      "amount_due": 79900,
      "status": "open",
      "attempt_count": 1
    }
  }
}

Webhook Endpoint

The webhook endpoint is:
POST https://api.contafy.com/api/webhooks/stripe
This endpoint does NOT require authentication. Stripe authenticates webhooks using the stripe-signature header.

Setting Up Webhooks

1

Create webhook in Stripe Dashboard

  1. Go to Stripe Dashboard > Developers > Webhooks
  2. Click “Add endpoint”
  3. Enter your webhook URL: https://api.contafy.com/api/webhooks/stripe
  4. Select API version (use latest)
2

Select events to listen to

Add these events:
  • checkout.session.completed
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.payment_succeeded
  • invoice.payment_failed
3

Copy webhook signing secret

After creating the webhook, Stripe provides a signing secret starting with whsec_.Copy this and add it to your environment:
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
4

Test the webhook

Use Stripe CLI or Dashboard to send test events and verify they’re received correctly.

Webhook Signature Verification

The API automatically verifies webhook signatures using Stripe’s SDK:
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
  req.body,
  sig,
  webhookSecret
);
Never disable signature verification in production. Without it, attackers could send fake webhook events to your server.

Missing Signature

{
  "error": "Missing stripe-signature header"
}

Invalid Signature

{
  "error": "Webhook Error: No signatures found matching the expected signature for payload"
}

Webhook Security

Raw Body Required

Stripe signature verification requires the raw request body. The server uses express.raw() middleware:
app.use(
  '/api/webhooks/stripe',
  express.raw({ type: 'application/json' })
);
The webhook endpoint receives a raw buffer, not parsed JSON. This is required for signature verification.

Environment Variables

Configure these environment variables:
# Stripe API keys
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx

# Stripe Price IDs
STRIPE_PRICE_BASIC_MONTHLY=price_xxxxxxxxxxxxx
STRIPE_PRICE_BASIC_ANNUAL=price_xxxxxxxxxxxxx
STRIPE_PRICE_PRO_MONTHLY=price_xxxxxxxxxxxxx
STRIPE_PRICE_PRO_ANNUAL=price_xxxxxxxxxxxxx

Event Logging

All webhook events are automatically logged to the payment_events table:
CREATE TABLE payment_events (
  id UUID PRIMARY KEY,
  stripe_event_id VARCHAR(255) UNIQUE NOT NULL,
  stripe_event_type VARCHAR(100) NOT NULL,
  event_data JSONB NOT NULL,
  processed_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);
Query events:
SELECT * FROM payment_events 
WHERE stripe_event_type = 'customer.subscription.updated'
ORDER BY processed_at DESC
LIMIT 10;

Webhook Response

Always return 200 OK to acknowledge receipt:
{
  "received": true
}
Stripe retries failed webhooks with exponential backoff:
  • Initial retry: 5 minutes
  • Subsequent retries: Up to 3 days
  • Automatic disable after 30 days of failures

Testing Webhooks

Using Stripe CLI

Install Stripe CLI and forward events to your local server:
stripe listen --forward-to localhost:3001/api/webhooks/stripe
Trigger test events:
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_succeeded

Using Stripe Dashboard

Send test webhook events from Stripe Dashboard > Webhooks:
  1. Click on your webhook endpoint
  2. Click “Send test webhook”
  3. Select event type
  4. Click “Send test webhook”

Webhook Flow

1

User completes checkout

User pays via Stripe Checkout and is redirected to success page.
2

Stripe sends webhook

Stripe immediately sends checkout.session.completed webhook to your server.
3

API processes webhook

API verifies signature, creates/updates subscription in database.
4

User subscription is active

Next API call shows updated subscription status and plan limits.

Handling Subscription Changes

Upgrade Flow

  1. User creates checkout session for higher plan
  2. Webhook: checkout.session.completed
  3. Webhook: customer.subscription.updated
  4. Subscription immediately upgraded
  5. Prorated charge applied

Downgrade Flow

  1. User changes plan in Customer Portal
  2. Webhook: customer.subscription.updated with cancel_at_period_end: false
  3. Current plan remains active until period end
  4. New plan activates at next billing cycle

Cancellation Flow

  1. User cancels in Customer Portal
  2. Webhook: customer.subscription.updated with cancel_at_period_end: true
  3. Subscription active until period end
  4. Webhook: customer.subscription.deleted at period end
  5. Automatic downgrade to FREE plan

Error Handling

Webhook handlers include comprehensive error handling:
try {
  await handleCheckoutSessionCompleted(event);
} catch (error) {
  console.error('Error en handleCheckoutSessionCompleted:', error);
  throw error; // Re-throw for Stripe retry
}
If an error occurs:
  1. Error is logged to console
  2. HTTP 500 returned to Stripe
  3. Stripe automatically retries the webhook
  4. PaymentEvent still recorded with error details

Monitoring Webhooks

Stripe Dashboard

Monitor webhook delivery in Stripe Dashboard > Webhooks:
  • View success/failure rates
  • Inspect individual webhook attempts
  • See retry history
  • Resend failed webhooks manually

Application Logs

All webhook events are logged with structured data:
INFO: Webhook received: checkout.session.completed
INFO: Suscripción actualizada para usuario 123e4567-e89b-12d3-a456-426614174000, plan: PRO

Best Practices

  1. Always verify signatures: Never skip signature verification
  2. Return 200 quickly: Process webhooks asynchronously if needed
  3. Handle idempotency: Use stripe_event_id to prevent duplicate processing
  4. Log everything: Store webhook events for debugging and auditing
  5. Test thoroughly: Use Stripe CLI to test all event types
  6. Monitor failures: Set up alerts for webhook failures in Stripe Dashboard
  7. Handle retries: Ensure your webhook handler is idempotent
  8. Secure the endpoint: Keep your webhook secret confidential

Common Issues

Webhook Not Receiving Events

1

Check URL accessibility

Ensure https://api.contafy.com/api/webhooks/stripe is publicly accessible.
2

Verify webhook secret

Confirm STRIPE_WEBHOOK_SECRET matches the Stripe Dashboard value.
3

Check Stripe Dashboard

Review webhook attempts and error messages in Stripe Dashboard.
4

Test with Stripe CLI

Use stripe listen to debug locally.

Signature Verification Failed

  • Check that STRIPE_WEBHOOK_SECRET is correct
  • Verify webhook endpoint uses express.raw() middleware
  • Ensure no middleware modifies the request body before webhook handler

Duplicate Events

Stripe may send the same event multiple times. Handle idempotency:
const existingEvent = await PaymentEvent.findOne({
  where: { stripe_event_id: event.id }
});

if (existingEvent) {
  // Already processed, return success
  return res.json({ received: true, skipped: true });
}

Next Steps

Build docs developers (and LLMs) love