Handle payment events in real-time using Stripe webhooks. FairePlace uses Stripe for credit purchases, and you can listen for webhook events to track payment status in your system.
Setup
Go to your Stripe Dashboard
Add a webhook endpoint pointing to your server
Select the events you want to receive
Save the webhook signing secret
Key events
Event Description Action checkout.session.completedPayment successful Credits added to balance charge.succeededCharge confirmed by bank Payment finalized charge.refundedRefund processed Credits deducted from balance charge.failedPayment failed No credits added
Webhook payload
{
"id" : "evt_1234567890" ,
"type" : "checkout.session.completed" ,
"data" : {
"object" : {
"id" : "cs_live_a1b2c3d4e5f6g7h8" ,
"payment_status" : "paid" ,
"amount_total" : 25000 ,
"currency" : "eur" ,
"metadata" : {
"tenant_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"package" : "10_credits" ,
"credits" : "10"
}
}
},
"created" : 1708425600
}
Verify webhook signatures
Always verify the Stripe signature to ensure the event is authentic and hasn't been tampered with:
Node.js
const stripe = require ( "stripe" )(process.env. STRIPE_SECRET_KEY );
app. post ( "/webhooks/stripe" , express. raw ({ type: "application/json" }), ( req , res ) => {
const sig = req.headers[ "stripe-signature" ];
let event;
try {
event = stripe.webhooks. constructEvent (
req.body,
sig,
process.env. STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console. error ( "Webhook signature verification failed:" , err.message);
return res. status ( 400 ). send ( `Webhook Error: ${ err . message }` );
}
switch (event.type) {
case "checkout.session.completed" :
const session = event.data.object;
console. log ( `Credits purchased: ${ session . metadata . credits } for tenant ${ session . metadata . tenant_id }` );
// Update your local records
break ;
case "charge.refunded" :
const charge = event.data.object;
console. log ( `Refund processed: ${ charge . amount_refunded / 100 } EUR` );
// Deduct credits from local records
break ;
}
res. json ({ received: true });
});
Python
import stripe
from flask import Flask, request, jsonify
stripe.api_key = os.environ[ "STRIPE_SECRET_KEY" ]
endpoint_secret = os.environ[ "STRIPE_WEBHOOK_SECRET" ]
@app.route ( "/webhooks/stripe" , methods = [ "POST" ])
def stripe_webhook ():
payload = request.get_data()
sig = request.headers.get( "Stripe-Signature" )
try :
event = stripe.Webhook.construct_event(payload, sig, endpoint_secret)
except stripe.error.SignatureVerificationError:
return "Invalid signature" , 400
if event[ "type" ] == "checkout.session.completed" :
session = event[ "data" ][ "object" ]
credits = int (session[ "metadata" ][ "credits" ])
tenant_id = session[ "metadata" ][ "tenant_id" ]
# Update your local records
return jsonify({ "received" : True })
Retry policy
Stripe retries webhook deliveries for up to 3 days with exponential backoff:
Attempt Delay 1st retry 5 minutes 2nd retry 30 minutes 3rd retry 2 hours 4th retry 8 hours 5th retry 1 day
Your endpoint should return a 2xx status code within 20 seconds. If it returns an error or times out, Stripe will retry.
Best practices
Always verify signatures — Never trust webhook payloads without verifying the Stripe signature
Return 200 quickly — Process events asynchronously if they require heavy computation
Handle duplicates — Stripe may send the same event more than once. Use the event id to deduplicate
Use the metadata — FairePlace includes tenant_id, package, and credits in the session metadata
Monitor failures — Set up alerts in Stripe Dashboard for failed webhook deliveries
Testing webhooks locally
Use the Stripe CLI to forward webhook events to your local server:
# Install Stripe CLI and login
stripe listen --forward-to localhost:3000/webhooks/stripe
# Trigger a test event
stripe trigger checkout.session.completed
Related
Last modified on February 21, 2026