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

EventDescriptionWhen Triggered
billing.transaction.succeededPayment confirmed on-chainAfter facilitator confirms payment
billing.transaction.failedPayment failedWhen 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.succeeded and billing.transaction.failed
  • Secret: Auto-generated secret for signature verification

2. Implement Your Handler

Your endpoint should:

  1. Verify the webhook signature
  2. Check for duplicate events (idempotency)
  3. Process the event
  4. Return 200 OK

Webhook Payload

Headers

HeaderDescription
X-Meshpay-Event-IdUnique event ID (use as idempotency key)
X-Meshpay-TimestampUNIX timestamp when event was created
X-Meshpay-SignatureHMAC-SHA256 signature for verification
Content-Typeapplication/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

FieldTypeDescription
idstringTransaction ID
statusstringsucceeded or failed
tx_hashstring | nullBlockchain transaction hash (null if failed before payment)
amountstringPayment amount
currencystringCurrency code
customer_refstring | nullYour customer reference
resource_refstring | nullYour resource reference
referencestring | nullOptional reference string
billing_flow_idstring | nullBilling flow ID (if charge was flow-based)
metadataobject | nullCustom metadata attached to the charge
confirmed_atstring | nullISO 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 hmac
import hashlib
def 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 signature
payload = 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'}), 401
event = request.json
# Process event...
return jsonify({'received': True}), 200

Handling Events

Complete Handler Example

from flask import Flask, request, jsonify
import hmac
import hashlib
import os
from datetime import datetime
app = Flask(__name__)
@app.route('/webhooks/meshpay', methods=['POST'])
def handle_webhook():
# 1. Verify signature
timestamp = 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 event
data = request.json
event_type = data.get('event')
event_data = data.get('data')
# 3. Check idempotency
existing_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 type
try:
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 processed
db.webhook_events.insert_one({
'event_id': event_id,
'event_type': event_type,
'processed_at': datetime.utcnow(),
})
return jsonify({'status': 'processed'}), 200
except Exception as e:
print(f'Error processing webhook: {e}')
return jsonify({'error': 'Processing failed'}), 500
def handle_payment_succeeded(data):
print(f"Payment succeeded: {data['id']}")
# Grant entitlement to user
if 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 email
if 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 failure
db.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

  1. Use HTTPS - Your webhook endpoint must use HTTPS
  2. Handle retries - If you return non-2xx, we'll retry with exponential backoff
  3. Process asynchronously - Queue heavy work to avoid timeout issues
  4. Store raw payloads - Keep the original webhook payload for debugging
  5. 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:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
624 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:

  1. Go to Developers → Webhooks
  2. Select your endpoint
  3. Click Send Test Event
  4. Choose the event type

Webhook Logs

View recent webhook deliveries in the Dashboard:

  1. Go to Developers → Webhooks
  2. Select your endpoint
  3. Click Delivery Logs

You'll see:

  • Delivery status (success/failed)
  • Response code
  • Response time
  • Request/response bodies

Troubleshooting

Webhooks Not Received

  1. Check endpoint URL - Ensure it's correct and accessible from the internet
  2. Check firewall - Allow incoming connections from Meshpay IPs
  3. Check HTTPS - Webhook URLs must use HTTPS
  4. Check event types - Ensure the events you want are selected

Signature Verification Failing

  1. Use raw body - Don't parse JSON before verification
  2. Check secret - Ensure you're using the correct webhook secret
  3. Check encoding - Body should be UTF-8 encoded

Duplicate Events

  1. Implement idempotency - Use X-Meshpay-Event-Id as a unique key
  2. Check processing logic - Ensure you're not processing the same event twice

Timeouts

  1. Return 200 quickly - Acknowledge receipt before heavy processing
  2. Use async processing - Queue work for background processing
  3. Increase timeout - If possible, increase your server's request timeout

Related Documentation