Developer Resources · Webhooks
Getting Started
This page walks through setting up your first webhook, from creating an endpoint to verifying that events arrive correctly. By the end, you will have a working webhook receiving events from Monime.
What you need before you start
Webhooks require an endpoint that Monime can reach over the public internet. Before you create a webhook in the dashboard, confirm that you have the following:
An HTTPS endpoint. Monime sends webhook deliveries only over HTTPS because payloads may contain sensitive data. The endpoint URL must start with https://. Plain HTTP endpoints are rejected.
A publicly routable domain name. The URL must use a domain name, not an IP address. URLs like https://192.168.1.1/webhooks or https://[::1]/webhooks are rejected. If you are testing locally, you need a tunnel service (see the local testing section below).
POST request handling. Your endpoint must accept HTTP POST requests. Monime sends event payloads in the request body as JSON.
Timely responses. Your endpoint must respond within Monime's timeout window. If processing takes longer than a few seconds, return a success response immediately and process the event asynchronously.
Access to the Developer section of your Space. Only users with developer or admin access can create webhooks. If you do not see the Developer menu item, contact your Space administrator.
Create a minimal endpoint
Start with a simple endpoint that accepts POST requests and returns a 200 response. This lets you confirm that Monime can reach your server before you add verification and business logic.
The following example uses Node.js with Express:
const express = require('express');
const app = express();
app.post('/webhooks/monime', express.raw({ type: 'application/json' }), (req, res) => {
console.log('Received webhook:', req.body.toString());
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});Notice that the endpoint uses express.raw() middleware. This preserves the raw request body, which you will need later for signature verification. If you parse the body as JSON before verification, re-serializing it may produce different bytes, causing verification to fail.
The following example uses Python with Flask:
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/monime', methods=['POST'])
def webhook():
raw_body = request.get_data()
print('Received webhook:', raw_body.decode())
return '', 200
if __name__ == '__main__':
app.run(port=3000)Flask's request.get_data() returns the raw body bytes, which you preserve for signature verification.
Expose your endpoint to the internet
If your server runs on a cloud provider with a public IP and domain, you can skip this section. If you are developing locally, Monime cannot reach localhost because that address refers to Monime's own servers, not yours.
Use a tunnel service to expose your local server. These services create a public URL that forwards requests to your local machine.
ngrok is a popular choice:
ngrok http 3000After running this command, ngrok displays a public URL like https://abc123.ngrok.io. Use this URL as your webhook endpoint. Requests to https://abc123.ngrok.io/webhooks/monime are forwarded to http://localhost:3000/webhooks/monime on your machine.
localtunnel is an open-source alternative:
npx localtunnel --port 3000Localtunnel provides a public URL in a similar fashion.
Keep the tunnel running while testing. When you stop the tunnel, the public URL becomes unreachable and webhook deliveries fail.
Create the webhook in the dashboard
With your endpoint running and reachable, create the webhook:
Open the Webhooks page
Navigate to Developer in your Space sidebar, then select Webhooks.
Click Create webhook
The create dialog opens with configuration fields.
Enter a name
Choose a name that identifies this webhook. For example, "Local Development Test" or "Production Payment Handler."
Enter the endpoint URL
Paste the full URL to your endpoint, including the path. For example, https://abc123.ngrok.io/webhooks/monime.
Select a verification method
Choose HMAC-SHA256 for now. Copy the generated secret and save it; you will need it to verify signatures.
Add alert emails (optional)
Add up to two email addresses for delivery-failure notifications.
Select an API release and events
Choose the latest API release. Select at least one event to subscribe to,
such as payment.processing_completed or checkout_session.completed.
Click Create
The webhook is created and enabled. Monime begins sending events immediately.
Trigger a test event
Create activity in your Space that matches one of the subscribed events. For
example, if you subscribed to payment.processing_completed, initiate a test
payment and complete it.
After the event occurs, return to the webhook details page in the dashboard. The Event Deliveries table shows a new row for the delivery. Click the row to see the full payload that Monime sent.
Your server logs should show the received webhook. If you see the payload in the logs and the dashboard shows status Success, your webhook is working.
Add signature verification
A working webhook should verify signatures before processing payloads. Without verification, anyone who discovers your endpoint URL can send forged requests.
For an HMAC-SHA256 webhook, verify the monime-signature header before
processing the event. The header contains a Unix timestamp (t) and a base64
signature (v1). Monime signs the timestamp, an underscore, and the raw request
body.
const crypto = require('crypto');
const express = require('express');
const app = express();
const secret = process.env.WEBHOOK_SECRET; // The secret you copied from the dashboard
function parseSignatureHeader(header) {
return Object.fromEntries(
header.split(',').map((part) => {
const [key, ...value] = part.split('=');
return [key.trim(), value.join('=')];
}),
);
}
app.post('/webhooks/monime', express.raw({ type: 'application/json' }), (req, res) => {
const signatureHeader = req.headers['monime-signature'];
const rawBody = req.body;
if (!signatureHeader) return res.status(401).send('Missing signature');
const { t, v1 } = parseSignatureHeader(signatureHeader);
if (!t || !v1) return res.status(401).send('Malformed signature');
if (!/^\d+$/.test(t)) return res.status(401).send('Malformed timestamp');
const timestamp = Number(t);
const now = Math.floor(Date.now() / 1000);
if (now - timestamp > 300 || timestamp - now > 60) {
return res.status(401).send('Timestamp out of tolerance');
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}_`)
.update(rawBody)
.digest();
const received = Buffer.from(v1, 'base64');
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(rawBody.toString());
console.log('Verified webhook:', payload.event.name, payload.event.id);
// Process the event here
res.status(200).send('OK');
});
app.listen(3000);Key points:
Decode v1 from base64 and use crypto.timingSafeEqual() only after checking
that both byte arrays have the same length.
Compute the signature over ${t}_ followed by the raw request bytes, not a
parsed and re-serialized JSON object. Parsing can reorder keys or change
whitespace, producing a different signature.
Return 401 if verification fails. This tells Monime the request was rejected and prevents processing of forged events.
Handle idempotency
Monime may deliver the same event multiple times due to retries, network glitches, or manual replays. Your handler must be idempotent: processing the same event twice should not cause incorrect behavior.
The standard approach is to track event IDs. When you receive an event, check whether you have already processed an event with that ID. If so, return success without re-processing.
const processedEvents = new Set(); // In production, use a database with TTL
app.post('/webhooks/monime', express.raw({ type: 'application/json' }), (req, res) => {
// ... signature verification ...
const payload = JSON.parse(rawBody.toString());
const eventId = payload.event.id;
if (processedEvents.has(eventId)) {
console.log('Duplicate event, skipping:', eventId);
return res.status(200).send('OK');
}
// Process the event
handleEvent(payload);
processedEvents.add(eventId);
res.status(200).send('OK');
});In production, store processed event IDs in a database with a time-to-live. The TTL should exceed Monime's retry window so that duplicates within that window are caught, but old entries are eventually cleaned up.
Common mistakes
Using HTTP instead of HTTPS. The dashboard rejects URLs that do not start with https://. Double-check the protocol.
Using localhost or an IP address. Monime cannot reach these endpoints. Use a domain name or a tunnel for local development.
Parsing the body before verification. If you use express.json() middleware before computing the signature, the raw bytes are lost. Use express.raw() and parse manually after verification.
Returning errors for events you do not care about. If you subscribe to events your handler does not need, return 200 anyway. Returning an error triggers retries for events you intended to ignore.
Slow handlers that cause timeouts. If your handler does heavy work before responding, Monime may time out and retry. Return 200 quickly and process asynchronously if needed.
Environment-specific webhooks
Create separate webhooks for each environment:
For development, use a tunnel URL pointing to your local machine. Enable only the events you are actively testing. Use a test secret that differs from production.
For staging, use your staging server's URL. Mirror the same event subscriptions as production so you can test realistic scenarios.
For production, use your production server's URL. Protect the secret carefully and monitor delivery health.
Separating environments prevents test events from polluting production systems and vice versa.
Next steps
With your first webhook working, explore these topics:
- Creating webhooks explains each configuration field in detail.
- Verification and security covers ECDSA P-256 and advanced security practices.
- Pre-launch checklist helps you validate your setup before going live.