Set Up Webhooks
Receive real-time notifications when orders are filled, deposits arrive, or withdrawals complete. This guide walks through the full setup from registration to testing.
Step 01Register a Webhook Endpoint
Call POST /api/v1/webhooks with your HTTPS URL and the events you want to receive. Merx returns a webhook ID and a signing secret.
curl -X POST https://merx.exchange/api/v1/webhooks \
-H "X-API-Key: $MERX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/merx",
"events": ["order.filled", "order.failed", "deposit.received"]
}'{
"id": "wh_abc123",
"url": "https://example.com/webhooks/merx",
"events": ["order.filled", "order.failed", "deposit.received"],
"secret": "whsec_your_signing_secret",
"status": "active"
}Save the secret immediately. It is only returned once. You need it to verify webhook signatures.
Handle Events in Express.js
Use express.raw() to receive the raw body for signature verification. Return 200 immediately and process the event asynchronously.
const express = require('express')
const crypto = require('crypto')
const app = express()
const WEBHOOK_SECRET = process.env.MERX_WEBHOOK_SECRET
app.post(
'/webhooks/merx',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-merx-signature'] || ''
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex')
const received = signature.replace('sha256=', '')
if (!crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex'),
)) {
return res.status(401).send('Invalid signature')
}
const event = JSON.parse(req.body)
console.log('Event:', event.event, event.data)
// Process asynchronously
res.status(200).send('OK')
},
)
app.listen(3000)Handle Events in Flask
import hmac
import hashlib
import os
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["MERX_WEBHOOK_SECRET"]
@app.route("/webhooks/merx", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Merx-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256,
).hexdigest()
received = signature.replace("sha256=", "")
if not hmac.compare_digest(expected, received):
return "Invalid signature", 401
event = request.get_json()
print("Event:", event["event"], event["data"])
# Process asynchronously
return "OK", 200Verify Signatures
Every webhook delivery includes an X-Merx-Signature header with format sha256=<hex_digest>. The digest is computed as HMAC-SHA256 of the raw request body using your webhook secret as the key.
- Always use the raw, unparsed request body for verification
- Use a timing-safe comparison function to prevent timing attacks
- Reject any request where the signature does not match
Test with curl
Compute a valid signature locally and send a test webhook to your endpoint. This lets you verify your handler works before going live.
SECRET="whsec_your_signing_secret"
BODY='{"event":"order.filled","data":{"id":"ord_test","status":"FILLED","amount":65000,"total_cost_sun":"5460000"},"timestamp":1711200000}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
curl -X POST http://localhost:3000/webhooks/merx \
-H "Content-Type: application/json" \
-H "X-Merx-Signature: sha256=$SIG" \
-d "$BODY"During development, use a tool like ngrok to expose your local server. Register the ngrok URL as your webhook endpoint to receive real events locally.
Event Reference
| Event | Trigger |
|---|---|
| order.filled | Energy order fully filled by providers |
| order.failed | Order could not be fulfilled |
| deposit.received | TRX deposit confirmed on-chain |
| withdrawal.completed | Withdrawal broadcast and confirmed |
For full payload examples of each event, see the events reference. For more on signature verification, see the verification guide.