CroissantPay sends webhooks to your server when subscription events occur. Use webhooks to update your database, send emails, or trigger other actions in real-time.
Setting Up Webhooks
- Configure your endpoint - Go to your app settings in the CroissantPay dashboard and enter your webhook URL.
- Save your secret - CroissantPay will generate a webhook secret. Store this securely - you'll need it to verify signatures.
- Handle events - Implement an endpoint that receives POST requests and processes events.
Event Types
CroissantPay uses RevenueCat-compatible event types, making migration easy. Events are triggered when Apple App Store or Google Play send notifications.
INITIAL_PURCHASENew subscription was purchased for the first timeNON_RENEWING_PURCHASEA non-renewing or one-time purchase was madeRENEWALSubscription was automatically renewedPRODUCT_CHANGEUser upgraded or downgraded their subscriptionCANCELLATIONUser turned off auto-renewal (will expire at period end)UNCANCELLATIONUser re-enabled auto-renewal before expirationBILLING_ISSUEPayment failed, subscription entering grace period or retryBILLING_ISSUE_RESOLVEDPayment issue was resolved, subscription is active againEXPIRATIONSubscription has expired and is no longer activeREFUNDPurchase was refunded or revoked by the storeTRIAL_STARTEDFree trial period startedTRIAL_CONVERTEDTrial converted to paid subscriptionTRIAL_CANCELLEDUser cancelled during trial (before conversion)GRACE_PERIOD_ENTEREDSubscription entered grace period due to billing issueGRACE_PERIOD_EXITEDGrace period ended (either resolved or expired)SUBSCRIPTION_PAUSEDUser paused their subscription (Google Play only)SUBSCRIPTION_RESUMEDUser resumed their paused subscriptionTESTTest webhook sent from the dashboardWebhook Payload
Every webhook event includes comprehensive subscriber, subscription, and entitlement data. This format is designed to be compatible with RevenueCat webhooks:
{
"api_version": "1.0",
"event": {
"id": "evt_abc123...",
"type": "RENEWAL",
"app_id": "app_xyz789...",
"event_timestamp_ms": 1705315800000,
// Complete subscriber information
"subscriber_info": {
"id": "sub_123...",
"app_user_id": "user_456",
"original_app_user_id": null,
"aliases": [],
"first_seen_at": "2024-01-01T00:00:00Z",
"last_seen_at": "2024-01-15T10:30:00Z",
"attributes": {}
},
// Product details
"product": {
"id": "prod_abc...",
"identifier": "pro_monthly",
"store_product_id": "com.yourapp.pro.monthly",
"platform": "ios",
"type": "auto_renewable_subscription",
"display_name": "Pro Monthly",
"subscription_period": "P1M",
"trial_period": "P7D"
},
// Subscription state
"subscription": {
"id": "subscription_123...",
"status": "active",
"original_transaction_id": "1000000123456789",
"latest_transaction_id": "1000000987654321",
"purchase_date": "2024-01-15T10:30:00Z",
"original_purchase_date": "2023-12-15T10:30:00Z",
"expires_date": "2024-02-15T10:30:00Z",
"auto_renew_enabled": true,
"is_trial_period": false,
"is_intro_period": false,
"canceled_at": null,
"cancellation_reason": null,
"grace_period_expires_date": null
},
// Current entitlements snapshot
"entitlements": [
{
"id": "ent_abc...",
"identifier": "premium",
"is_active": true,
"expires_date": "2024-02-15T10:30:00Z",
"product_identifier": "pro_monthly"
}
],
"platform": "ios",
"environment": "production",
// Original store notification info
"store_event": {
"type": "DID_RENEW",
"event_id": "apple_notification_uuid"
}
}
}Verifying Signatures
Always verify webhook signatures to ensure events are from CroissantPay. We include a signature in the X-CroissantPay-Signature header.
Using the SDK (Node.js)
import { verifyWebhookSignature, constructEvent } from '@croissantpay/react-native';
app.post('/webhooks/croissantpay', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-croissantpay-signature'];
const payload = req.body.toString();
try {
const event = constructEvent(
payload,
signature,
process.env.CROISSANTPAY_WEBHOOK_SECRET
);
// Handle the event
switch (event.type) {
case 'subscription.renewed':
await handleRenewal(event.data);
break;
case 'subscription.expired':
await handleExpiration(event.data);
break;
// ... handle other events
}
res.json({ received: true });
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send('Webhook Error');
}
});Manual Verification
import crypto from 'crypto';
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', '')),
Buffer.from(expectedSignature)
);
}Best Practices
Return 200 quickly
Return a 200 response as soon as possible. Process the webhook asynchronously if needed.
Handle duplicates
Use the event ID to detect and handle duplicate deliveries. Store processed event IDs.
Implement idempotency
Make sure processing the same event twice doesn't cause issues.
Use HTTPS
Always use HTTPS endpoints in production.
Monitor failures
Set up alerting for webhook failures. CroissantPay retries failed webhooks up to 3 times.
Retry Policy
If your endpoint doesn't return a 2xx response, we'll retry:
- 1st retry: After 2 seconds
- 2nd retry: After 4 seconds
- 3rd retry: After 8 seconds
After 3 failed attempts, the webhook will be marked as failed. You can view failed webhooks in the dashboard.
Testing Webhooks
Use the "Send Test" button in your app settings to send a test webhook to your endpoint. This helps verify your integration before going live.
For local development, you can use tools like ngrok to expose your local server to the internet.