Webhook Notifications
Webhooks enable your application to receive real-time notifications when events occur in Cashela. Instead of polling the API for updates, webhooks push data to your server immediately when a transaction status changes, payment completes, or other important events happen.
Overview
How Webhooks Work
- Configure your endpoint – Provide a callback URL when creating transactions
- Event occurs – A transaction status changes (e.g., deposit completed)
- Cashela sends notification – HTTP POST request to your callback URL
- You process and respond – Return 2xx status to acknowledge receipt
- Retry on failure – If we don’t receive 2xx, we retry with exponential backoff
Benefits
- Real-time updates – Know immediately when transactions complete
- Reduced API calls – No need to poll for status changes
- Reliability – Automatic retries ensure delivery
- Security – Cryptographic signatures verify authenticity
Webhook Delivery
Request Format
All webhook notifications are sent as HTTP POST requests with:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Cashela-Signature | HMAC-SHA256 signature of the request body |
X-Cashela-Event | Event type (e.g., transaction.completed) |
X-Cashela-Delivery-Id | Unique identifier for this delivery attempt |
X-Cashela-Timestamp | Unix timestamp when the webhook was sent |
Retry Policy
If your endpoint doesn’t return a 2xx status code, Cashela will retry:
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0 seconds |
| 2 | 5 seconds | 5 seconds |
| 3 | 30 seconds | 35 seconds |
| 4 | 2 minutes | ~2.5 minutes |
| 5 | 15 minutes | ~17 minutes |
| 6 | 1 hour | ~1.3 hours |
| 7 | 4 hours | ~5.3 hours |
| 8 | 24 hours | ~29 hours |
After 8 failed attempts, the webhook is marked as failed and no further retries are made.
Tip: If you consistently miss webhooks, check your server logs, firewall settings, and SSL certificate configuration.
Signature Verification
Every webhook includes a cryptographic signature that you must verify before processing. This ensures the request originated from Cashela and wasn’t tampered with.
Signature Header
X-Cashela-Signature: sha256=5d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7eVerification Steps
- Extract the raw request body (before any parsing)
- Compute HMAC-SHA256 using your Business Secret as the key
- Compare your computed signature with the header value
- Reject the request if signatures don’t match
Implementation Examples
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, secret) { const expectedSignature = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody, 'utf8') .digest('hex');
// Use constant-time comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) );}
// Express.js middlewareapp.post('/webhook/cashela', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-cashela-signature']; const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, signature, process.env.CASHELA_SECRET)) { console.error('Invalid webhook signature'); return res.status(401).json({ error: 'Invalid signature' }); }
// Process the webhook const event = JSON.parse(rawBody); handleWebhookEvent(event);
res.status(200).json({ received: true });});Python (Flask)
import hmacimport hashlibfrom flask import Flask, request, jsonify
app = Flask(__name__)CASHELA_SECRET = os.environ.get('CASHELA_SECRET')
def verify_signature(payload, signature, secret): expected = 'sha256=' + hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected)
@app.route('/webhook/cashela', methods=['POST'])def handle_webhook(): signature = request.headers.get('X-Cashela-Signature') raw_body = request.get_data()
if not verify_signature(raw_body, signature, CASHELA_SECRET): return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json() process_webhook_event(event)
return jsonify({'received': True}), 200PHP
<?phpfunction verifyWebhookSignature($payload, $signature, $secret) { $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret); return hash_equals($expected, $signature);}
// Handle incoming webhook$rawBody = file_get_contents('php://input');$signature = $_SERVER['HTTP_X_CASHELA_SIGNATURE'] ?? '';
if (!verifyWebhookSignature($rawBody, $signature, CASHELA_SECRET)) { http_response_code(401); echo json_encode(['error' => 'Invalid signature']); exit;}
$event = json_decode($rawBody, true);processWebhookEvent($event);
http_response_code(200);echo json_encode(['received' => true]);?>Event Types
PayIn Events
| Event | Description |
|---|---|
payin.deposit.created | Deposit request created, awaiting payment |
payin.deposit.pending | Payment initiated, processing |
payin.deposit.completed | Payment successfully received |
payin.deposit.failed | Payment failed or was declined |
payin.deposit.expired | Payment window expired without completion |
payin.deposit.refunded | Payment was refunded to customer |
PayOut Events
| Event | Description |
|---|---|
payout.transaction.created | Payout request created |
payout.transaction.processing | Payout is being processed |
payout.transaction.completed | Funds delivered to beneficiary |
payout.transaction.failed | Payout failed (insufficient funds, invalid account, etc.) |
payout.transaction.cancelled | Payout was cancelled |
Webhook Payload Structure
Standard Payload Format
{ "id": "evt_01HJ3KBCD8E9F0G1H2I3J4K5L6", "type": "payin.deposit.completed", "api_version": "2024-01-01", "created": "2024-01-15T10:30:00.000Z", "data": { "object": { "id": "dep_01HJ3KBCD8E9F0G1H2I3J4K5L6", "status": "COMPLETED", "amount": 1500.00, "currency": "MXN", "payment_method": "CARD", "external_identifier": "order_12345", "metadata": { "order_id": "12345", "customer_id": "cust_789" } }, "previous_status": "PENDING" }, "request_id": "req_01HJ3KBCD8E9F0G1H2I3J4K5L6"}Payload Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for this event |
type | string | Event type (see Event Types above) |
api_version | string | API version used to generate the event |
created | string | ISO-8601 timestamp when event occurred |
data.object | object | The resource that triggered the event |
data.previous_status | string | Previous status (for status change events) |
request_id | string | Original API request ID (if applicable) |
Handling Webhooks
Best Practices
-
Respond quickly
Return a 2xx response as soon as you receive the webhook. Process the event asynchronously.
app.post('/webhook', async (req, res) => {// Acknowledge immediatelyres.status(200).json({ received: true });// Process asynchronouslyqueueWebhookProcessing(req.body);}); -
Handle idempotency
Webhooks may be delivered multiple times. Use the event
idto deduplicate.async function processWebhook(event) {// Check if already processedconst existing = await db.webhookEvents.findOne({ eventId: event.id });if (existing) {console.log(`Event ${event.id} already processed, skipping`);return;}// Process and mark as handledawait handleEvent(event);await db.webhookEvents.create({ eventId: event.id, processedAt: new Date() });} -
Verify before trusting
Always verify the signature before processing any webhook data.
-
Handle unknown events gracefully
function handleEvent(event) {switch (event.type) {case 'payin.deposit.completed':handleDepositCompleted(event.data.object);break;case 'payout.transaction.completed':handlePayoutCompleted(event.data.object);break;default:// Log but don't fail on unknown eventsconsole.log(`Unhandled event type: ${event.type}`);}} -
Implement logging
Log all webhook activity for debugging and auditing.
async function logWebhook(event, status) {await db.webhookLogs.create({eventId: event.id,eventType: event.type,receivedAt: new Date(),status: status,payload: JSON.stringify(event)});}
Error Recovery
If your server was down and missed webhooks:
- Check transaction status via API – Use the Get Transaction endpoint to fetch current status
- Review webhook logs – Failed deliveries are logged in your Cashela dashboard
- Reconcile periodically – Run daily reconciliation to catch any missed events
Security Checklist
- HTTPS only – Never accept webhooks over plain HTTP
- Verify signatures – Always validate
X-Cashela-Signatureheader - Use constant-time comparison – Prevent timing attacks when comparing signatures
- Protect your endpoint – Use firewall rules to limit access if possible
- Don’t trust data blindly – Validate all data from webhooks
- Rotate secrets – Periodically rotate your Business Secret
Testing Webhooks
Local Development
Use tools like ngrok or localtunnel to expose your local server:
# Install ngroknpm install -g ngrok
# Expose local portngrok http 3000Use the generated HTTPS URL as your callback URL for testing.
Webhook Simulation
In sandbox mode, you can trigger test webhooks from the Cashela dashboard to verify your implementation.
Troubleshooting
| Issue | Possible Cause | Solution |
|---|---|---|
| No webhooks received | Firewall blocking requests | Allow Cashela IP ranges |
| Signature verification failing | Using parsed body instead of raw | Use raw request body for signature |
| Duplicate events processed | Not checking event ID | Store and check event IDs |
| 5xx errors on your endpoint | Slow processing | Respond immediately, process async |
| SSL certificate errors | Invalid or expired cert | Update SSL certificate |
Related Documentation
- Error Handling – Understanding error responses
- PayIn Webhooks – PayIn-specific webhook details
- Idempotency – Handling duplicate events