Webhooks
For custom integrations, Croissant sends a webhook to your server when a payment is confirmed. This is how you know a customer completed a purchase through Croissant Pay.
Overview
When a Croissant Pay session reaches the CHARGED state, Croissant sends an HTTP POST to the webhook URL you have configured. This confirms that the customer's payment has been captured.
Setup
Webhook URL configuration is currently managed by the Croissant team. Contact your partnership manager or email partnerships@croissant.com to register your endpoint.
You will provide:
- A publicly accessible HTTPS endpoint that accepts POST requests
- Separate URLs for sandbox and production environments (recommended)
Payload
Croissant sends a JSON payload with the following structure:
{
"event": "payment.confirmed",
"sessionId": "cps_abc123def456",
"retailerId": "your-retailer-id",
"checkoutId": "checkout_xyz789",
"checkoutUrl": "https://your-store.com/checkout/abc123",
"amounts": {
"total": 29500,
"cash": 20000,
"credit": 5000,
"incentive": 4500,
"shipping": 0,
"tax": 0,
"currency": "USD"
},
"lineItems": [
{
"retailerProductId": "SKU-001",
"name": "Leather Tote Bag",
"brand": "Your Brand",
"quantity": 1,
"totalPrice": 29500
}
],
"customer": {
"email": "customer@example.com"
},
"timestamp": "2026-03-25T12:00:00.000Z"
}Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | Event type. Currently always "payment.confirmed". |
sessionId | string | Unique Croissant Pay session identifier. |
retailerId | string | Your merchant identifier. |
checkoutId | string | The checkout ID you passed when the component was initialized. |
checkoutUrl | string | The checkout URL you provided. |
amounts.total | number | Total order amount in cents. |
amounts.cash | number | Amount charged to customer's card (cents). |
amounts.credit | number | Croissant credit applied (cents). |
amounts.incentive | number | Incentive discount applied (cents). |
amounts.shipping | number | Shipping amount (cents). |
amounts.tax | number | Tax amount (cents). |
amounts.currency | string | ISO 4217 currency code (e.g., USD). |
lineItems | array | Products in the order. |
customer.email | string | Customer's email address. |
timestamp | string | ISO 8601 timestamp of the payment confirmation. |
Response & Retries
Your endpoint should return a 200 status code to acknowledge receipt. Croissant will retry on failure with exponential backoff (up to 5 attempts over 24 hours).
Signature Verification
Croissant signs every webhook request using HMAC-SHA256. Verify the signature to ensure the request is authentic and has not been tampered with.
| Header | Description |
|---|---|
X-Croissant-Signature | HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. |
X-Croissant-Timestamp | Unix timestamp (seconds) when the signature was generated. Reject requests older than 5 minutes. |
Example Handler
import crypto from 'crypto';
import express from 'express';
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
const WEBHOOK_SECRET = process.env.CROISSANT_WEBHOOK_SECRET;
app.post('/webhooks/croissant', (req, res) => {
// 1. Verify signature
const signature = req.headers['x-croissant-signature'];
const timestamp = req.headers['x-croissant-timestamp'];
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(timestamp + '.' + req.rawBody)
.digest('hex');
if (signature !== expected) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Check timestamp freshness (reject > 5 minutes)
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (age > 300) {
return res.status(401).json({ error: 'Request too old' });
}
// 3. Process the event
const { event, sessionId, amounts, lineItems, customer } = req.body;
if (event === 'payment.confirmed') {
// Create the order in your system
console.log('Croissant Pay confirmed:', {
sessionId,
total: amounts.total,
email: customer.email,
items: lineItems.length,
});
// Your order creation logic here...
}
res.status(200).json({ received: true });
});sessionId with your order record. You will need it when calling the Order Lifecycle API for fulfillments and cancellations.