Webhooks
Settlra sends HTTP POST requests to your registered endpoints whenever a payout changes state. Webhooks are the recommended way to track payout progress — no polling required.
Registering an endpoint
Register your webhook URL in the Dashboard → Settings → Webhooks or via the API:
"color:#ff7b72">curl "color:#79c0ff">-X POST https://api-sandbox.settlra.com/v1/webhooks/endpoints \ "color:#79c0ff">-H "Authorization: Bearer sk_sandbox_your_key" \ "color:#79c0ff">-H "Content-Type: application/json" \ "color:#79c0ff">-d '{ "url": "https://yourapp.com/webhooks/settlra", "events": ["payout.settled", "payout.failed", "payout.compliance_hold"], "description": "Production payout notifications" }'
{
"id": "wh_01j3ab4cd5ef6gh7ij8kl9mn0p",
"url": "https://yourapp.com/webhooks/settlra",
"events": ["payout.settled", "payout.failed", "payout.compliance_hold"],
"secret": "whsec_ABCDef123456...",
"is_active": true,
"created_at": "2024-07-01T12:00:00.000Z"
}Store the secret securely — you'll use it to verify webhook signatures.
Event types
| Event | Description |
|---|---|
payout.created | A new payout has been created and is awaiting USDC. |
payout.funds_received | USDC was confirmed on-chain. Processing begins. |
payout.initiated | Mobile money transfer has been sent to the provider. |
payout.settled | Mobile money delivered to recipient. Final state. |
payout.failed | Payout failed after all retry attempts. Final state. |
payout.compliance_hold | Compliance flag raised; manual review required. |
deposit.received | USDC deposit detected on the assigned address. |
quote.expired | A quote expired without being used. |
Webhook payload structure
{
"id": "evt_01j3pq8rs9tu0vw1xy2za3bc4d",
"type": "payout.settled",
"created_at": "2024-07-01T12:04:35.000Z",
"data": {
"payout_id": "pyt_01j3pq8rs9tu0vw1xy2za3bc4d",
"status": "SETTLED",
"source_amount_usdc": 500,
"target_amount_fiat": 1871250,
"target_currency": "UGX",
"exchange_rate": 3742.5,
"recipient_phone": "+256700123456",
"network": "MTN_UG",
"provider_reference": "FLW-TXN-12345678",
"settled_at": "2024-07-01T12:04:35.000Z"
}
}Signature verification
Every webhook request includes an X-Settlra-Signature header. This is an HMAC-SHA256 signature of the raw request body, signed with your webhook secret. Always verify the signature before processing the event.
Node.js verification
"color:#ff7b72">import crypto "color:#ff7b72">from 'crypto'; "color:#ff7b72">import express "color:#ff7b72">from 'express'; "color:#ff7b72">const app = express(); "color:#8b949e">// IMPORTANT: use raw body parser — JSON.parse changes byte content app.use('/webhooks/settlra', express.raw({ "color:#ff7b72">type: 'application/json' })); app.post('/webhooks/settlra', (req, res) => { "color:#ff7b72">const signature = req.headers['x-settlra-signature']; "color:#ff7b72">if (!signature) { "color:#ff7b72">return res.status(400).send('Missing signature header'); } "color:#8b949e">// Compute expected signature "color:#ff7b72">const expectedSignature = 'sha256=' + crypto .createHmac('sha256', process.env.SETTLRA_WEBHOOK_SECRET) .update(req.body) "color:#8b949e">// req.body is Buffer when using raw parser .digest('hex'); "color:#8b949e">// Use timingSafeEqual to prevent timing attacks "color:#ff7b72">const sigBuffer = Buffer."color:#ff7b72">from(signature); "color:#ff7b72">const expectedBuffer = Buffer."color:#ff7b72">from(expectedSignature); "color:#ff7b72">if ( sigBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(sigBuffer, expectedBuffer) ) { "color:#ff7b72">return res.status(401).send('Invalid signature'); } "color:#ff7b72">const event = JSON.parse(req.body.toString()); switch (event."color:#ff7b72">type) { case 'payout.settled': "color:#ff7b72">await handlePayoutSettled(event.data); break; case 'payout.failed': "color:#ff7b72">await handlePayoutFailed(event.data); break; case 'payout.compliance_hold': "color:#ff7b72">await alertComplianceTeam(event.data); break; } "color:#8b949e">// Always "color:#ff7b72">return 200 quickly res.status(200).json({ received: "color:#79c0ff">true }); });
Python verification
"color:#ff7b72">import hmac "color:#ff7b72">import hashlib "color:#ff7b72">import json "color:#ff7b72">from flask "color:#ff7b72">import Flask, request, jsonify "color:#ff7b72">import os app = Flask(__name__) @app.route('/webhooks/settlra', methods=['POST']) "color:#ff7b72">def handle_webhook(): signature = request.headers.get('X-Settlra-Signature', '') raw_body = request.get_data() "color:#8b949e"># raw bytes "color:#8b949e"># Compute expected signature secret = os.environ['SETTLRA_WEBHOOK_SECRET'].encode() expected = 'sha256=' + hmac.new( secret, raw_body, hashlib.sha256 ).hexdigest() "color:#8b949e"># Constant-time comparison "color:#ff7b72">if not hmac.compare_digest(signature, expected): "color:#ff7b72">return jsonify({'error': 'Invalid signature'}), 401 event = json.loads(raw_body) "color:#ff7b72">if event['type'] == 'payout.settled': handle_payout_settled(event['data']) "color:#ff7b72">elif event['type'] == 'payout.failed': handle_payout_failed(event['data']) "color:#ff7b72">return jsonify({'received': "color:#79c0ff">True}), 200
Retry behavior
If your endpoint doesn't return HTTP 200–299 within 10 seconds, Settlra retries with exponential backoff:
| Attempt | Delay after previous attempt |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| Final (4th) | 2 hours |
After 5 total failed delivery attempts, the webhook delivery is marked as failed. You can resend specific events from the dashboard.
Webhook endpoint management API
| Method | Path | Description |
|---|---|---|
GET | /v1/webhooks/endpoints | List your webhook endpoints |
POST | /v1/webhooks/endpoints | Register a new endpoint |
GET | /v1/webhooks/endpoints/:id | Get one endpoint |
PUT | /v1/webhooks/endpoints/:id | Update URL or events |
DELETE | /v1/webhooks/endpoints/:id | Delete an endpoint |
POST | /v1/webhooks/endpoints/:id/test | Send a test event |
Best practices
- Respond quickly. Return
200immediately and process the event asynchronously (e.g., in a queue). Settlra times out after 10 seconds. - Handle duplicates. Webhooks may be delivered more than once. Make your handler idempotent — check the
idfield. - Use HTTPS. Production endpoints must use valid TLS. Self-signed certs are rejected.
- Don't trust the payload alone. Always verify the signature. An attacker can POST arbitrary payloads to your webhook URL.