Billing Webhooks
Webhooks notify your server in real-time when billing events occur. Use them to update your database, grant entitlements, send notifications, or trigger downstream processes.
Webhook Events
| Event | Description | When Triggered |
|---|---|---|
billing.transaction.succeeded | Payment confirmed on-chain | After facilitator confirms payment |
billing.transaction.failed | Payment failed | When payment fails or times out |
Setting Up Webhooks
1. Create a Webhook Endpoint
In your Dashboard, go to Developers → Webhooks and click Add Endpoint.
Configure:
- URL: Your server endpoint (e.g.,
https://api.yoursite.com/webhooks/meshpay) - Events: Select
billing.transaction.succeededandbilling.transaction.failed - Secret: Auto-generated secret for signature verification
2. Implement Your Handler
Your endpoint should:
- Verify the webhook signature
- Check for duplicate events (idempotency)
- Process the event
- Return 200 OK
Webhook Payload
Headers
| Header | Description |
|---|---|
X-Meshpay-Event-Id | Unique event ID (use as idempotency key) |
X-Meshpay-Timestamp | UNIX timestamp when event was created |
X-Meshpay-Signature | HMAC-SHA256 signature for verification |
Content-Type | application/json |
Body Structure
{
"event": "billing.transaction.succeeded",
"data": {
"id": "3de45a76-4b5b-400f-9d40-7fee8557e76d",
"status": "succeeded",
"tx_hash": "3ehzyYwXiDZW1xFJTfrY41stbmpzqWWCLo1QphjMTY6a...",
"amount": "0.99",
"currency": "USD",
"customer_ref": "user_123",
"resource_ref": "article:premium-guide-42",
"reference": "order_456",
"billing_flow_id": "flow_abc",
"metadata": {
"plan": "pro",
"source": "website"
},
"confirmed_at": "2025-12-01T12:40:07.594403+00:00"
},
"timestamp": "2025-12-01T12:40:08.000Z"
}
Event Data Fields
| Field | Type | Description |
|---|---|---|
id | string | Transaction ID |
status | string | succeeded or failed |
tx_hash | string | null | Blockchain transaction hash (null if failed before payment) |
amount | string | Payment amount |
currency | string | Currency code |
customer_ref | string | null | Your customer reference |
resource_ref | string | null | Your resource reference |
reference | string | null | Optional reference string |
billing_flow_id | string | null | Billing flow ID (if charge was flow-based) |
metadata | object | null | Custom metadata attached to the charge |
confirmed_at | string | null | ISO 8601 timestamp when payment was confirmed |
Signature Verification
Always verify webhook signatures to ensure the request came from Meshpay.
Signature Format
The signature is computed as:
HMAC-SHA256(webhook_secret, timestamp + "." + raw_body)
Verification Code
import hmacimport hashlibdef verify_webhook_signature(request, secret):timestamp = request.headers.get('X-Meshpay-Timestamp')signature = request.headers.get('X-Meshpay-Signature')body = request.get_data(as_text=True) # Raw body# Compute expected signaturepayload = f"{timestamp}.{body}"expected_signature = hmac.new(secret.encode(),payload.encode(),hashlib.sha256).hexdigest()# Compare signatures (timing-safe)return hmac.compare_digest(signature, expected_signature)# Usage in Flask@app.route('/webhooks/meshpay', methods=['POST'])def handle_webhook():is_valid = verify_webhook_signature(request,os.environ['MESHPAY_WEBHOOK_SECRET'])if not is_valid:return jsonify({'error': 'Invalid signature'}), 401event = request.json# Process event...return jsonify({'received': True}), 200
Handling Events
Complete Handler Example
from flask import Flask, request, jsonifyimport hmacimport hashlibimport osfrom datetime import datetimeapp = Flask(__name__)@app.route('/webhooks/meshpay', methods=['POST'])def handle_webhook():# 1. Verify signaturetimestamp = request.headers.get('X-Meshpay-Timestamp')signature = request.headers.get('X-Meshpay-Signature')event_id = request.headers.get('X-Meshpay-Event-Id')body = request.get_data(as_text=True)expected_signature = hmac.new(os.environ['MESHPAY_WEBHOOK_SECRET'].encode(),f"{timestamp}.{body}".encode(),hashlib.sha256).hexdigest()if not hmac.compare_digest(signature, expected_signature):print('Invalid webhook signature')return jsonify({'error': 'Invalid signature'}), 401# 2. Parse eventdata = request.jsonevent_type = data.get('event')event_data = data.get('data')# 3. Check idempotencyexisting_event = db.webhook_events.find_one({'event_id': event_id})if existing_event:print(f'Event {event_id} already processed')return jsonify({'status': 'already_processed'}), 200# 4. Process event based on typetry:if event_type == 'billing.transaction.succeeded':handle_payment_succeeded(event_data)elif event_type == 'billing.transaction.failed':handle_payment_failed(event_data)else:print(f'Unhandled event type: {event_type}')# 5. Mark event as processeddb.webhook_events.insert_one({'event_id': event_id,'event_type': event_type,'processed_at': datetime.utcnow(),})return jsonify({'status': 'processed'}), 200except Exception as e:print(f'Error processing webhook: {e}')return jsonify({'error': 'Processing failed'}), 500def handle_payment_succeeded(data):print(f"Payment succeeded: {data['id']}")# Grant entitlement to userif data.get('customer_ref') and data.get('resource_ref'):db.entitlements.update_one({'user_id': data['customer_ref'],'resource_id': data['resource_ref'],},{'$set': {'transaction_id': data['id'],'amount': data['amount'],'currency': data['currency'],'granted_at': datetime.utcnow(),}},upsert=True)# Send confirmation emailif data.get('customer_ref'):send_payment_confirmation_email(data['customer_ref'], data)def handle_payment_failed(data):print(f"Payment failed: {data['id']}")# Log the failuredb.payment_failures.insert_one({'transaction_id': data['id'],'customer_ref': data.get('customer_ref'),'resource_ref': data.get('resource_ref'),'amount': data['amount'],'currency': data['currency'],'failed_at': datetime.utcnow(),})
Best Practices
Verify Signatures
Always verify the X-Meshpay-Signature header to ensure webhooks are authentic.
Idempotent Handling
Use X-Meshpay-Event-Id to prevent duplicate processing. Webhooks may be retried.
Return 200 Quickly
Return 200 OK quickly. Do heavy processing asynchronously to avoid timeouts.
Log Everything
Log all webhook events for debugging and audit purposes.
Additional Recommendations
- Use HTTPS - Your webhook endpoint must use HTTPS
- Handle retries - If you return non-2xx, we'll retry with exponential backoff
- Process asynchronously - Queue heavy work to avoid timeout issues
- Store raw payloads - Keep the original webhook payload for debugging
- Monitor failures - Set up alerts for webhook processing failures
Retry Policy
If your endpoint returns a non-2xx status code or times out, we'll retry:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 24 hours |
After 6 failed attempts, the webhook is marked as failed and won't be retried.
Testing Webhooks
Local Development
Use a tool like ngrok to expose your local server:
ngrok http 3000
Then configure the ngrok URL as your webhook endpoint in the Dashboard.
Test Events
You can trigger test events from the Dashboard:
- Go to Developers → Webhooks
- Select your endpoint
- Click Send Test Event
- Choose the event type
Webhook Logs
View recent webhook deliveries in the Dashboard:
- Go to Developers → Webhooks
- Select your endpoint
- Click Delivery Logs
You'll see:
- Delivery status (success/failed)
- Response code
- Response time
- Request/response bodies
Troubleshooting
Webhooks Not Received
- Check endpoint URL - Ensure it's correct and accessible from the internet
- Check firewall - Allow incoming connections from Meshpay IPs
- Check HTTPS - Webhook URLs must use HTTPS
- Check event types - Ensure the events you want are selected
Signature Verification Failing
- Use raw body - Don't parse JSON before verification
- Check secret - Ensure you're using the correct webhook secret
- Check encoding - Body should be UTF-8 encoded
Duplicate Events
- Implement idempotency - Use
X-Meshpay-Event-Idas a unique key - Check processing logic - Ensure you're not processing the same event twice
Timeouts
- Return 200 quickly - Acknowledge receipt before heavy processing
- Use async processing - Queue work for background processing
- Increase timeout - If possible, increase your server's request timeout
Related Documentation
- Seller Integration Guide - Complete integration guide
- Payment Verification - Real-time verification API
- Charges API - Creating charges
- Transactions - Listing transactions