Why webhooks matter for payments
When a customer pays via bank transfer or mobile money, the payment is asynchronous. Your server initiates the transaction, but the result arrives minutes (sometimes hours) later via a webhook. If your webhook handler is unreliable, you will miss successful payments, leaving customers frustrated and your revenue unrecorded.
Crezaro delivers webhooks for every significant event in the payment lifecycle: payment success, payment failure, refund processed, dispute opened, subscription renewed, and more. Your job is to handle them correctly.
Always verify the signature
Every webhook Crezaro sends includes an x-crezaro-signature header. This is an HMAC-SHA512 hash of the request body, computed using your secret key. Always verify this signature before processing the webhook.
// Node.js example
const crypto = require('crypto');
function verifyWebhook(body, signature, secretKey) {
const hash = crypto
.createHmac('sha512', secretKey)
.update(body)
.digest('hex');
return hash === signature;
}
// In your Express handler:
app.post('/webhooks/crezaro', express.raw({ type: '*/*' }), (req, res) => {
const isValid = verifyWebhook(
req.body,
req.headers['x-crezaro-signature'],
process.env.CREZARO_SECRET_KEY
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process the event...
res.status(200).send('OK');
});
Important: use the raw request body for signature verification, not a parsed-and-re-serialized version. JSON serialization is not deterministic, and re-serializing can change the byte sequence.
Make your handler idempotent
Crezaro guarantees at-least-once delivery. This means the same webhook event may be delivered more than once, for example if your server responded with a timeout or a 5xx error. Your handler must be able to process the same event multiple times without side effects.
The simplest pattern is to track processed event IDs:
// PHP / Laravel example
public function handleWebhook(Request $request)
{
$event = $request->json();
$eventId = $event['id'];
// Check if we have already processed this event
if (ProcessedWebhook::where('event_id', $eventId)->exists()) {
return response('Already processed', 200);
}
// Process the event
$this->processPaymentEvent($event);
// Record that we processed it
ProcessedWebhook::create(['event_id' => $eventId]);
return response('OK', 200);
}
Respond quickly, process asynchronously
Crezaro expects a response within 30 seconds. If your handler takes longer, the delivery is marked as failed and will be retried. Do not perform heavy processing in the webhook handler itself. Instead, acknowledge receipt immediately and process the event asynchronously.
// Good: acknowledge immediately, process in background
app.post('/webhooks/crezaro', (req, res) => {
// Verify signature...
// Queue for async processing
eventQueue.add(req.body);
// Respond immediately
res.status(200).send('OK');
});
Handle retries gracefully
Crezaro retries failed webhook deliveries with exponential backoff:
- 1st retry: 5 minutes after initial failure
- 2nd retry: 30 minutes
- 3rd retry: 2 hours
- 4th retry: 8 hours
- 5th retry: 24 hours
After five failed attempts, the webhook is marked as permanently failed and appears in your dashboard under Developers > Webhooks > Failed. You can manually retry from there.
To minimize retries, ensure your endpoint:
- Returns a 200 status code on success (any 2xx is accepted)
- Does not redirect (3xx responses are treated as failures)
- Is accessible from the internet (not behind a VPN or firewall that blocks our IPs)
- Responds within 30 seconds
Use webhook events, not callbacks, as your source of truth
When a customer completes payment, they are redirected to your callback URL. It is tempting to use this redirect as confirmation that the payment succeeded. Do not do this. The callback URL redirect can be spoofed, and the customer might close their browser before the redirect happens.
The correct flow is:
- Customer completes payment and is redirected to your callback URL
- Your callback page shows a "Processing..." state
- Your server receives the webhook confirming the payment status
- Your server updates the order status
- The callback page polls your API and updates to show "Payment confirmed"
Testing webhooks locally
During development, your local machine is not accessible from the internet. Use a tunneling service like ngrok to expose your local webhook endpoint:
# Start your local server
npm start # Runs on port 3000
# In another terminal, expose it
ngrok http 3000
# Use the ngrok URL as your webhook URL in the Crezaro dashboard
# https://abc123.ngrok.io/webhooks/crezaro
Alternatively, use the Crezaro CLI to forward webhook events directly to your local server without a tunneling service. See our CLI documentation for setup instructions.
Checklist
Before going live, confirm that your webhook implementation covers these points:
- Signature verification on every request
- Idempotent processing (duplicate events are safe)
- Fast acknowledgment (process heavy work asynchronously)
- Error handling that does not leak sensitive information
- Logging of all received events for debugging
- Monitoring and alerting on webhook processing failures
Questions? Our developer support team is available at developers@crezaro.com or through the chat widget in your dashboard.