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:

json
{
  "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

FieldTypeDescription
eventstringEvent type. Currently always "payment.confirmed".
sessionIdstringUnique Croissant Pay session identifier.
retailerIdstringYour merchant identifier.
checkoutIdstringThe checkout ID you passed when the component was initialized.
checkoutUrlstringThe checkout URL you provided.
amounts.totalnumberTotal order amount in cents.
amounts.cashnumberAmount charged to customer's card (cents).
amounts.creditnumberCroissant credit applied (cents).
amounts.incentivenumberIncentive discount applied (cents).
amounts.shippingnumberShipping amount (cents).
amounts.taxnumberTax amount (cents).
amounts.currencystringISO 4217 currency code (e.g., USD).
lineItemsarrayProducts in the order.
customer.emailstringCustomer's email address.
timestampstringISO 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.

HeaderDescription
X-Croissant-SignatureHMAC-SHA256 hex digest of the raw request body, signed with your webhook secret.
X-Croissant-TimestampUnix timestamp (seconds) when the signature was generated. Reject requests older than 5 minutes.

Example Handler

javascript
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 });
});
Tip
Store the sessionId with your order record. You will need it when calling the Order Lifecycle API for fulfillments and cancellations.

Next: Manage order lifecycle →