Webhooks allow your application to receive real-time notifications when events occur in your Paygentic account. When enabled, we’ll send HTTP POST requests to your configured endpoints whenever relevant events happen, such as customer creation, subscription changes, and more.
Quickstart Guide
Get up and running with webhooks in just a few minutes:
Configure Your Endpoint
Once enabled, click “Manage Webhook Endpoints” to access the webhook management portal where you can:
Add your webhook endpoint URL (e.g., https://your-domain.com/webhooks/paygentic
)
Select which event types to subscribe to
View your webhook signing secret
Test Your Endpoint
Use tools like Hookdeck or ngrok to test webhooks during development without deploying to production.
Verify and Process Events
import express from 'express' ;
import { Webhook } from 'svix' ;
const app = express ();
const webhookSecret = process . env . PAYGENTIC_WEBHOOK_SECRET ; // Your signing secret from webhook portal
app . post ( '/webhooks/paygentic' ,
express . raw ({ type: 'application/json' }), // Important: Use raw body
async ( req , res ) => {
const headers = {
'svix-id' : req . headers [ 'svix-id' ],
'svix-timestamp' : req . headers [ 'svix-timestamp' ],
'svix-signature' : req . headers [ 'svix-signature' ]
};
try {
const wh = new Webhook ( webhookSecret );
const payload = wh . verify ( req . body , headers );
// Process the webhook
console . log ( 'Webhook verified:' , payload );
// Handle different event types
switch ( payload . eventType ) {
case 'customer.created.v0' :
await handleCustomerCreated ( payload . data );
break ;
case 'subscription.created.v0' :
await handleSubscriptionCreated ( payload . data );
break ;
case 'source_event.pending.v0' :
await handleSourceEventPending ( payload . data );
break ;
// Add more cases as needed
}
res . status ( 200 ). send ( 'OK' );
} catch ( err ) {
console . error ( 'Webhook verification failed:' , err );
res . status ( 400 ). send ( 'Webhook verification failed' );
}
}
);
Setting Up Webhooks
Enabling Webhooks
Access the Dashboard : Log in to your Paygentic account and navigate to Developer > Webhooks
Enable Webhooks : Toggle the “Enable Webhooks” switch to activate webhook functionality
Access Management Portal : Click “Manage Webhook Endpoints” to open the webhook management portal
Configuring Endpoints
In the webhook management portal, you can:
Add Endpoints : Specify the URL where you want to receive webhook events
Select Event Types : Choose which events you want to subscribe to
Set Filters : Configure advanced filtering rules if needed
View Logs : Monitor webhook deliveries and debug any issues
Retrieve Signing Secret : Access your webhook signing secret for verification
Remember to keep your webhook signing secret secure and never commit it to version control. Store it in environment variables or a secure secrets management system.
Best Practices
Use HTTPS endpoints only (HTTP is not supported for security reasons)
Implement webhook processing asynchronously to respond quickly
Store the svix-id
to handle duplicate events (idempotency)
Disable CSRF protection for your webhook endpoint
Respond with a 2xx status code within 15 seconds
Security & Verification
Webhook security is critical to ensure that the events you receive are legitimate and haven’t been tampered with. Paygentic signs all webhooks with HMAC-SHA256.
Every webhook request includes three important headers:
svix-id
: Unique identifier for the webhook message (use for idempotency)
svix-timestamp
: Unix timestamp when the webhook was sent
svix-signature
: Base64 encoded signature(s) for verification
Signature Verification
Always verify webhook signatures in production. This prevents attackers from sending fake events to your endpoint.
The recommended approach is to use Svix’s verification library, which we recommend for easy and secure webhook verification:
Verification Examples
import { Webhook } from 'svix' ;
function verifyWebhook ( body , headers , secret ) {
const wh = new Webhook ( secret );
try {
// Verify the webhook - throws on error
const payload = wh . verify ( body , headers );
return { verified: true , payload };
} catch ( err ) {
return { verified: false , error: err . message };
}
}
// Important: Use the raw request body
// Many frameworks parse JSON automatically - you need the raw string
app . use ( '/webhooks' , express . raw ({ type: 'application/json' }));
Replay Attack Protection
The timestamp in the svix-timestamp
header protects against replay attacks. The Svix library automatically rejects webhooks with timestamps more than 5 minutes old (past or future). Ensure your server’s clock is synchronized using NTP.
Handling Webhooks
Response Requirements
Status Code : Return a 2xx status code (200-299) to indicate successful receipt
Timeout : Respond within 15 seconds or the delivery will be considered failed
Body : The response body is ignored - a simple “OK” or empty response is fine
Processing Best Practices
app . post ( '/webhooks/paygentic' , async ( req , res ) => {
// 1. Verify the webhook (as shown above)
const payload = verifyWebhook ( req . body , headers , secret );
// 2. Respond immediately
res . status ( 200 ). send ( 'OK' );
// 3. Process asynchronously (don't block the response)
setImmediate ( async () => {
try {
await processWebhookAsync ( payload );
} catch ( error ) {
console . error ( 'Error processing webhook:' , error );
// Log to your error tracking service
}
});
});
Idempotency
Use the svix-id
header to ensure you only process each event once:
const processedEvents = new Set (); // In production, use Redis or a database
async function processWebhook ( payload , svixId ) {
if ( processedEvents . has ( svixId )) {
console . log ( `Event ${ svixId } already processed, skipping` );
return ;
}
// Process the event
await handleEvent ( payload );
// Mark as processed
processedEvents . add ( svixId );
}
CSRF Protection
Disable CSRF protection for webhook endpoints. Webhooks are verified using signatures, not CSRF tokens.
// Disable CSRF for webhook route
app . use ( '/webhooks' , ( req , res , next ) => {
req . csrfToken = () => '' ; // Disable CSRF
next ();
});
Retry Policy & Failure Handling
Automatic Retry Schedule
If your endpoint fails to respond successfully, we will retry with exponential backoff:
Attempt Delay After Previous 1 Immediately 2 5 seconds 3 5 minutes 4 30 minutes 5 2 hours 6 5 hours 7 10 hours 8 10 hours
Failure Scenarios
A webhook delivery is considered failed if:
Your endpoint returns a non-2xx status code
Your endpoint doesn’t respond within 15 seconds
The endpoint is unreachable (connection error)
Your endpoint returns a 3xx redirect (not followed)
Endpoint Disabling
If an endpoint fails continuously for 5 days, it will be automatically disabled. You’ll receive an operational webhook notification when this happens. You can re-enable the endpoint from the webhook management portal.
Manual Recovery
You can manually retry failed webhooks through the webhook management portal:
Individual Retry : Retry a specific failed message
Bulk Recovery : Replay all failed messages from a specific date
View Logs : Inspect delivery attempts and error messages
Event Types Reference
Paygentic currently supports the following webhook event types:
Triggered when a new customer is successfully created. {
"eventType" : "customer.created.v0" ,
"eventId" : "evt_Wqb1k73rXprtTm7Qdlr38G" ,
"timestamp" : "2024-01-15T10:30:00.000Z" ,
"data" : {
"customerId" : "cust_123abc" ,
"merchantId" : "org_456def" ,
"createdAt" : "2024-01-15T10:30:00.000Z"
}
}
customer.creation_failed.v0
Triggered when customer creation fails. {
"eventType" : "customer.creation_failed.v0" ,
"eventId" : "evt_Xrc2l84sYqstUn8Reis49H" ,
"timestamp" : "2024-01-15T10:31:00.000Z" ,
"data" : {
"merchantId" : "org_456def" ,
"error" : {
"code" : "validation_failed" ,
"message" : "Required field 'email' is missing"
},
"attemptedAt" : "2024-01-15T10:31:00.000Z"
}
}
Triggered when a new subscription is created. {
"eventType" : "subscription.created.v0" ,
"eventId" : "evt_Zsd3m95tZrtuVo9Sfjt51I" ,
"timestamp" : "2024-01-15T11:00:00.000Z" ,
"data" : {
"subscriptionId" : "sub_789ghi" ,
"customerId" : "cust_123abc" ,
"planId" : "plan_456def" ,
"planName" : "Professional Plan" ,
"merchantId" : "org_456def" ,
"status" : "active" ,
"startedAt" : "2024-01-15T11:00:00.000Z"
}
}
subscription.cancelled.v0
Triggered when a subscription is cancelled. {
"eventType" : "subscription.cancelled.v0" ,
"eventId" : "evt_Ate4n06uAsuVwp0Tgku62J" ,
"timestamp" : "2024-01-15T12:00:00.000Z" ,
"data" : {
"subscriptionId" : "sub_789ghi" ,
"customerId" : "cust_123abc" ,
"merchantId" : "org_456def" ,
"cancelledAt" : "2024-01-15T12:00:00.000Z" ,
"reason" : "customer_request"
}
}
Triggered when a subscription is updated (e.g., plan change, quantity adjustment). {
"eventType" : "subscription.updated.v0" ,
"eventId" : "evt_Buf5o17vBtvWxq1Uhlv73K" ,
"timestamp" : "2024-01-15T13:00:00.000Z" ,
"data" : {
"subscriptionId" : "sub_789ghi" ,
"customerId" : "cust_123abc" ,
"merchantId" : "org_456def" ,
"updates" : {
"status" : "paused" ,
"planId" : "plan_789ghi"
},
"updatedAt" : "2024-01-15T13:00:00.000Z"
}
}
Triggered when a usage source is successfully activated and connected. {
"eventType" : "source.activated.v0" ,
"eventId" : "evt_Cuf6p28wCuwXyr2Vim84L" ,
"timestamp" : "2024-01-15T14:00:00.000Z" ,
"data" : {
"sourceId" : "src_abc123" ,
"sourceType" : "stripe_revenue" ,
"subscriptionId" : "sub_789ghi" ,
"customerId" : "cust_123abc" ,
"merchantId" : "org_456def" ,
"provider" : "stripe" ,
"activatedAt" : "2024-01-15T14:00:00.000Z"
}
}
source.activation_failed.v0
Triggered when a usage source fails to activate due to configuration or authentication issues. {
"eventType" : "source.activation_failed.v0" ,
"eventId" : "evt_Dvg7q39xDvxYzs3Wjo95M" ,
"timestamp" : "2024-01-15T14:05:00.000Z" ,
"data" : {
"sourceId" : "src_abc123" ,
"sourceType" : "stripe_revenue" ,
"subscriptionId" : "sub_789ghi" ,
"customerId" : "cust_123abc" ,
"merchantId" : "org_456def" ,
"error" : {
"code" : "authentication_failed" ,
"message" : "Invalid API key provided"
},
"attemptedAt" : "2024-01-15T14:05:00.000Z"
}
}
Triggered when a usage source is disconnected or deactivated. {
"eventType" : "source.disconnected.v0" ,
"eventId" : "evt_Ewh8r40yEwyZat4Xkp06N" ,
"timestamp" : "2024-01-15T15:00:00.000Z" ,
"data" : {
"sourceId" : "src_abc123" ,
"sourceType" : "stripe_revenue" ,
"subscriptionId" : "sub_789ghi" ,
"customerId" : "cust_123abc" ,
"merchantId" : "org_456def" ,
"reason" : "user_initiated" ,
"disconnectedAt" : "2024-01-15T15:00:00.000Z"
}
}
Triggered when a source event enters pending state and requires manual processing. {
"eventType" : "source_event.pending.v0" ,
"eventId" : "evt_SrcEvt1k73rXprtTm7Qdlr38G" ,
"timestamp" : "2024-01-15T10:30:00.000Z" ,
"data" : {
"sourceEventId" : "sev_123abc" ,
"sourceId" : "src_456def" ,
"sourceType" : "stripe_revenue" ,
"planId" : "plan_789ghi" ,
"subscriptionId" : "sub_abc123" ,
"merchantId" : "org_def456" ,
"externalEventId" : "inv_stripe_xyz789" ,
"status" : "pending" ,
"createdAt" : "2024-01-15T10:30:00.000Z" ,
"updatedAt" : "2024-01-15T10:30:00.000Z"
}
}
source_event.processed.v0
Triggered when a source event is successfully processed into usage events. {
"eventType" : "source_event.processed.v0" ,
"eventId" : "evt_SrcEvt2k73rXprtTm7Qdlr38G" ,
"timestamp" : "2024-01-15T10:35:00.000Z" ,
"data" : {
"sourceEventId" : "sev_123abc" ,
"sourceId" : "src_456def" ,
"sourceType" : "stripe_revenue" ,
"planId" : "plan_789ghi" ,
"subscriptionId" : "sub_abc123" ,
"merchantId" : "org_def456" ,
"externalEventId" : "inv_stripe_xyz789" ,
"status" : "processed" ,
"previousStatus" : "pending" ,
"usageEventIds" : [ "ue_111aaa" , "ue_222bbb" ],
"processedBy" : "user_xyz123" ,
"processedAt" : "2024-01-15T10:35:00.000Z" ,
"createdAt" : "2024-01-15T10:30:00.000Z" ,
"updatedAt" : "2024-01-15T10:35:00.000Z"
}
}
Triggered when a source event fails to process due to errors. {
"eventType" : "source_event.failed.v0" ,
"eventId" : "evt_SrcEvt3k73rXprtTm7Qdlr38G" ,
"timestamp" : "2024-01-15T10:32:00.000Z" ,
"data" : {
"sourceEventId" : "sev_456def" ,
"sourceId" : "src_789ghi" ,
"sourceType" : "stripe_revenue" ,
"planId" : "plan_abc123" ,
"subscriptionId" : "sub_def456" ,
"merchantId" : "org_ghi789" ,
"externalEventId" : "inv_stripe_abc456" ,
"status" : "failed" ,
"previousStatus" : "pending" ,
"errorMessage" : "Unable to transform event data: missing required field 'amount'" ,
"createdAt" : "2024-01-15T10:30:00.000Z" ,
"updatedAt" : "2024-01-15T10:32:00.000Z"
}
}
Triggered when a source event is manually rejected by a user. {
"eventType" : "source_event.rejected.v0" ,
"eventId" : "evt_SrcEvt4k73rXprtTm7Qdlr38G" ,
"timestamp" : "2024-01-15T10:40:00.000Z" ,
"data" : {
"sourceEventId" : "sev_789ghi" ,
"sourceId" : "src_abc123" ,
"sourceType" : "stripe_revenue" ,
"planId" : "plan_def456" ,
"subscriptionId" : "sub_ghi789" ,
"merchantId" : "org_jkl012" ,
"externalEventId" : "inv_stripe_def789" ,
"status" : "rejected" ,
"previousStatus" : "pending" ,
"rejectionReason" : "Duplicate invoice detected" ,
"processedBy" : "user_abc456" ,
"processedAt" : "2024-01-15T10:40:00.000Z" ,
"createdAt" : "2024-01-15T10:30:00.000Z" ,
"updatedAt" : "2024-01-15T10:40:00.000Z"
}
}
Common Event Fields
All webhook events include these standard fields:
Field Type Description eventType
string The type of event (e.g., customer.created.v0
) eventId
string Unique identifier for this specific event timestamp
string ISO 8601 timestamp when the event occurred data
object Event-specific data payload
Code Examples
Complete Webhook Handler
Here’s a production-ready webhook handler example:
import express from 'express' ;
import { Webhook } from 'svix' ;
import Redis from 'ioredis' ;
const app = express ();
const redis = new Redis ( process . env . REDIS_URL );
const webhookSecret = process . env . PAYGENTIC_WEBHOOK_SECRET ;
// Webhook handler with all best practices
app . post ( '/webhooks/paygentic' ,
express . raw ({ type: 'application/json' }),
async ( req , res ) => {
const svixId = req . headers [ 'svix-id' ];
const svixTimestamp = req . headers [ 'svix-timestamp' ];
const svixSignature = req . headers [ 'svix-signature' ];
// Check required headers
if ( ! svixId || ! svixTimestamp || ! svixSignature ) {
return res . status ( 400 ). send ( 'Missing required headers' );
}
try {
// Verify webhook signature
const wh = new Webhook ( webhookSecret );
const payload = wh . verify ( req . body , {
'svix-id' : svixId ,
'svix-timestamp' : svixTimestamp ,
'svix-signature' : svixSignature
});
// Check for duplicate processing (idempotency)
const processed = await redis . get ( `webhook: ${ svixId } ` );
if ( processed ) {
console . log ( `Webhook ${ svixId } already processed` );
return res . status ( 200 ). send ( 'Already processed' );
}
// Mark as processed (with 24 hour expiry)
await redis . setex ( `webhook: ${ svixId } ` , 86400 , 'processed' );
// Respond immediately
res . status ( 200 ). send ( 'OK' );
// Process asynchronously
setImmediate ( async () => {
try {
await processWebhookEvent ( payload );
} catch ( error ) {
console . error ( 'Error processing webhook:' , error );
// Send to error tracking service
// Consider implementing a retry queue
}
});
} catch ( err ) {
console . error ( 'Webhook verification failed:' , err );
return res . status ( 400 ). send ( 'Invalid webhook' );
}
}
);
async function processWebhookEvent ( payload ) {
const { eventType , data } = payload ;
console . log ( `Processing event: ${ eventType } ` );
switch ( eventType ) {
case 'customer.created.v0' :
await handleCustomerCreated ( data );
break ;
case 'customer.creation_failed.v0' :
await handleCustomerCreationFailed ( data );
break ;
case 'subscription.created.v0' :
await handleSubscriptionCreated ( data );
break ;
case 'subscription.cancelled.v0' :
await handleSubscriptionCancelled ( data );
break ;
case 'subscription.updated.v0' :
await handleSubscriptionUpdated ( data );
break ;
case 'source_event.pending.v0' :
await handleSourceEventPending ( data );
break ;
default :
console . log ( `Unhandled event type: ${ eventType } ` );
}
}
// Event handlers
async function handleCustomerCreated ( data ) {
console . log ( 'New customer created:' , data . customerId );
// Your business logic here
}
async function handleSubscriptionCreated ( data ) {
console . log ( 'New subscription created:' , data . subscriptionId );
// Your business logic here
}
async function handleSourceEventPending ( data ) {
console . log ( 'Source event requires manual processing:' , data . sourceEventId );
// Your business logic here
}
// ... implement other handlers
Testing Webhooks Locally
For local development, use a tunneling service to expose your local server:
# Using ngrok
ngrok http 3000
# Using Hookdeck CLI
hookdeck listen 3000
# Your webhook URL will be something like:
# https://abc123.ngrok.io/webhooks/paygentic
Then add this URL as your webhook endpoint in the webhook management portal for testing.
Additional Resources
Need Help?
If you encounter any issues with webhooks:
Check the delivery logs in the webhook management portal for error messages
Verify your endpoint is returning a 2xx status code
Ensure you’re using the correct signing secret
Confirm your server’s clock is synchronized (for timestamp validation)
Contact support if you need additional assistance