Skip to main content
POST
/
api
/
payments
/
webhook
# This is how Stripe calls your webhook
curl -X POST https://api.vaniykempire.com/api/payments/webhook \
  -H "stripe-signature: t=1678886400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "evt_1MtwBwLkdIwHu7ix28a3tqPa",
    "type": "payment_intent.succeeded",
    "data": {
      "object": {
        "id": "pi_3MtwBwLkdIwHu7ix28a3tqPa",
        "amount": 2999,
        "currency": "usd",
        "status": "succeeded",
        "metadata": {
          "contentId": "507f191e810c19729de860ea",
          "userId": "507f1f77bcf86cd799439011",
          "contentTitle": "Advanced React Patterns"
        }
      }
    }
  }'
{
  "received": true
}
Secure webhook endpoint that receives events from Stripe when payment status changes. This endpoint handles payment intent success and failure events, updating the Purchase record status accordingly.

Important Notes

This endpoint requires raw request body parsing and must be placed before the express.json() middleware in your Express application. The Stripe signature verification requires access to the raw request body.
This endpoint is called by Stripe servers, not by your frontend application. Configure this webhook URL in your Stripe Dashboard under Developers → Webhooks.

Webhook Configuration

  1. In Stripe Dashboard, go to Developers → Webhooks
  2. Click “Add endpoint”
  3. Enter URL: https://api.vaniykempire.com/api/payments/webhook
  4. Select events to listen for:
    • payment_intent.succeeded
    • payment_intent.payment_failed
  5. Copy the webhook signing secret to STRIPE_WEBHOOK_SECRET environment variable

Signature Verification

The endpoint verifies webhook authenticity using Stripe’s signature verification:
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
  req.body,
  sig,
  process.env.STRIPE_WEBHOOK_SECRET
);

Handled Events

payment_intent.succeeded

Fired when a payment successfully completes. The webhook handler:
  1. Finds the Purchase record by stripePaymentIntentId
  2. Updates Purchase status to completed
  3. Logs the successful payment

payment_intent.payment_failed

Fired when a payment fails (card declined, insufficient funds, etc.). The webhook handler:
  1. Finds the Purchase record by stripePaymentIntentId
  2. Updates Purchase status to failed
  3. Logs the failed payment

Request Headers

stripe-signature
string
required
Stripe webhook signature for verifying the request authenticity. Automatically added by Stripe.
content-type
string
Should be application/json

Request Body

The raw Stripe event object. Common fields include:
id
string
Unique identifier for the event (e.g., evt_1MtwBwLkdIwHu7ix28a3tqPa)
type
string
Event type (e.g., payment_intent.succeeded, payment_intent.payment_failed)
data
object
Event data containing the payment intent object

Response

received
boolean
Always returns true to acknowledge receipt of the webhook

Error Handling

If signature verification fails, returns:
  • Status Code: 400
  • Body: Webhook Error: <error message>
# This is how Stripe calls your webhook
curl -X POST https://api.vaniykempire.com/api/payments/webhook \
  -H "stripe-signature: t=1678886400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "evt_1MtwBwLkdIwHu7ix28a3tqPa",
    "type": "payment_intent.succeeded",
    "data": {
      "object": {
        "id": "pi_3MtwBwLkdIwHu7ix28a3tqPa",
        "amount": 2999,
        "currency": "usd",
        "status": "succeeded",
        "metadata": {
          "contentId": "507f191e810c19729de860ea",
          "userId": "507f1f77bcf86cd799439011",
          "contentTitle": "Advanced React Patterns"
        }
      }
    }
  }'
{
  "received": true
}

Security Best Practices

  1. Always verify webhook signatures - Prevents unauthorized requests from spoofing Stripe
  2. Use raw body parsing - Required for signature verification
  3. Store webhook secret securely - Use environment variables, never commit to git
  4. Implement idempotency - Handle duplicate webhook events gracefully
  5. Return 200 quickly - Stripe will retry if endpoint times out

Webhook Retry Logic

Stripe automatically retries failed webhooks:
  • If endpoint returns non-2xx status code
  • If endpoint times out (after 30 seconds)
  • Retries for up to 3 days with exponential backoff
  • View retry attempts in Stripe Dashboard

Testing

Local Development

# Install Stripe CLI
stripe login

# Forward webhooks to local server
stripe listen --forward-to localhost:3000/api/payments/webhook

# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger payment_intent.payment_failed

Test Mode

Use Stripe test mode webhook endpoints with test API keys to safely test without affecting production data.

Troubleshooting

IssueSolution
Signature verification failsEnsure raw body parsing is enabled and webhook secret is correct
Purchase not foundCheck that payment intent was created through your API
Webhooks not receivedVerify webhook URL is correct in Stripe Dashboard
Duplicate eventsImplement idempotency using event ID

Environment Variables

STRIPE_WEBHOOK_SECRET
string
required
Webhook signing secret from Stripe Dashboard. Format: whsec_...

Implementation Notes

  • Middleware: express.raw({ type: 'application/json' })
  • Must be registered before express.json() middleware
  • Unhandled event types are logged but don’t cause errors
  • Purchase updates are wrapped in try-catch to prevent webhook failures
  • Source: src/controllers/paymentController.js:64-130
  • Route configuration: src/routes/paymentRoutes.js:7-11

Build docs developers (and LLMs) love