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

MethodPathPurpose
POST/developers/apps/{app_id}/webhookCreate or update the URL
GET/developers/apps/{app_id}/webhookView current config + secret
POST/developers/apps/{app_id}/webhook/rotateRotate the secret
DELETE/developers/apps/{app_id}/webhookRemove it
GET/developers/apps/{app_id}/webhook/deliveriesRecent delivery attempts, for debugging

Webhook Events

EventWhenAction
payment.completedPayment successfulCredit 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:

HeaderValue
X-CamelPay-EventEvent type, e.g. payment.completed
X-CamelPay-TimestampUnix timestamp (seconds)
X-CamelPay-Signaturehex(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}), 200

Retries 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.