Core
Webhooks
Receive real-time notifications for payment events pushed to your own endpoint. Verify signatures and handle retries.
Register a webhook URL on your developer app and CamelPay pushes payment.completed events to it — no polling needed. This is per-app, JWT-managed, and separate from your API key.
Register a Webhook
bash
curl -X POST https://api.camelpay.in/developers/apps/{app_id}/webhook \
-H "Authorization: Bearer <your_jwt_token>" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.com/webhooks/camelpay"}'The response includes your signing secret — copy it now. Unlike API keys, GET returns the secret again later too, since it only lets you verify payloads, not access your account.
Manage Your Webhook
| Method | Path | Purpose |
|---|---|---|
POST | /developers/apps/{app_id}/webhook | Create or update the URL |
GET | /developers/apps/{app_id}/webhook | View current config + secret |
POST | /developers/apps/{app_id}/webhook/rotate | Rotate the secret |
DELETE | /developers/apps/{app_id}/webhook | Remove it |
GET | /developers/apps/{app_id}/webhook/deliveries | Recent delivery attempts, for debugging |
Webhook Events
| Event | When | Action |
|---|---|---|
payment.completed | Payment successful | Credit your own records, fulfill order |
Webhook Payload
json
{
"id": "evt_abc123",
"type": "payment.completed",
"created_at": "2026-01-15T14:20:00Z",
"data": {
"reference": "cp_ref_abc123",
"payment_page_id": "page-uuid",
"amount": 2000,
"currency": "TZS",
"completed_at": "2026-01-15T14:20:00Z"
}
}Verify Webhook Signatures
Every delivery is signed with HMAC-SHA256 using your webhook secret:
| Header | Value |
|---|---|
X-CamelPay-Event | Event type, e.g. payment.completed |
X-CamelPay-Timestamp | Unix timestamp (seconds) |
X-CamelPay-Signature | hex(HMAC-SHA256(secret, "{timestamp}.{raw_body}")) |
Always verify signatures to prevent spoofed requests. Use the raw request body and constant-time comparison.
import hmac
import hashlib
def verify_webhook_signature(raw_body, timestamp, signature, secret):
message = f"{timestamp}.{raw_body.decode('utf-8')}"
expected = hmac.new(
secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Handle Events
Return 200 OK immediately and process events asynchronously:
python
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_here" # from /developers/apps/{app_id}/webhook
@app.route('/webhooks/camelpay', methods=['POST'])
def handle_webhook():
timestamp = request.headers.get('X-CamelPay-Timestamp', '')
signature = request.headers.get('X-CamelPay-Signature', '')
raw_body = request.data
message = f"{timestamp}.{raw_body.decode('utf-8')}"
expected = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
return jsonify({'error': 'Invalid signature'}), 401
event = json.loads(raw_body)
if event['type'] == 'payment.completed':
process_completed_payment(event['data'])
return jsonify({'received': True}), 200Retries and Idempotency
Deliveries are retried with exponential backoff (up to 5 attempts) if your endpoint doesn't return 2xx. Deduplicate by storing processed event IDs, since retries can mean the same event id arrives more than once:
python
processed_events = set()
def handle_webhook(event):
event_id = event['id']
if event_id in processed_events:
return {'status': 'already_processed'}
process_event(event)
processed_events.add(event_id)
return {'status': 'processed'}If delivery attempts fail, do not create a second payment. Treat GET /v1/transactions and GET /v1/payments/{reference}/status as recovery views. CamelPay also reconciles recent unresolved provider records every five minutes, and all wallet credits remain idempotent.
