Webhooks
Webhooks report changes to your system right away - to your ERP, your CRM, your own service, or straight into Slack and Discord.
Setting up
Webhooks are included from the Starter plan (see pricing). Create them in the app under API & Webhooks → Add webhook:
- Target URL:
https://only, and only publicly reachable addresses. Internal addresses such aslocalhostor private networks are refused. - Events: one or more from the list below.
- Format: JSON (signed, for your own software), Slack or Discord (a ready-made message for a channel).
- Status: active or paused.
Send test sends a test event right away and shows your server's answer. Recent deliveries lists the last 20 deliveries with status, HTTP code and error.
Events
| Event | When | data contains |
|---|---|---|
location.created | location created (in the app or through the API) or restored from the archive | id, name, street, zip, city, country, status, categoryId |
location.updated | location changed | as above |
location.deleted | location archived | as above |
map.published | map published | id, name, token, url |
lead.created | new enquiry through the contact form of a map | id, name, email, phone, type, message, locationId, location (name), mapId |
import.finished | CSV import finished | count, ids (at most 500) |
Locations from a CSV import do not send one location.created per row but one import.finished. Fetch the complete record of a location through the REST API when you need it.
What arrives (JSON format)
Every event arrives as a POST with Content-Type: application/json:
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: MyStoreLocator-Webhooks/1.0 (+https://my-store-locator.com)
X-MSL-Event: location.created
X-MSL-Delivery: evt_1842
X-MSL-Signature: t=1792958400,sha256=5dccb3580b7f5727afacb98d93052372bbd60fccdb1166666b35cf3ac78e3c02
{"id":"evt_1842","event":"location.created","createdAt":"2026-10-25T19:20:00.000Z","data":{"id":1003,"name":"Manchester store","street":"1 Market St","zip":"M1 1AA","city":"Manchester","country":"GB","status":"st_active_7","categoryId":12}}| Header | Meaning |
|---|---|
X-MSL-Event | name of the event |
X-MSL-Delivery | ID of the delivery, same as id in the body. It stays the same on retries - use it to detect duplicate deliveries |
X-MSL-Signature | t=<unix time>,sha256=<HMAC> to check authenticity |
The test event from the app has event: "test" and data: { "test": true }.
Verifying the signature
You find the key in the app with the webhook under Signing secret (HMAC-SHA256). The signature is an HMAC-SHA256 over <t>.<body> - the timestamp from the header, a dot and the unchanged request body, as a hex string. Check the body before you parse it as JSON, and reject old timestamps so nobody can replay a captured delivery.
Node.js (Express):
const crypto = require('crypto');
const express = require('express');
const app = express();
function verify(rawBody, header, secret) {
const m = /^t=(\d+),sha256=([a-f0-9]{64})$/.exec(header || '');
if (!m) return false;
if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false; // older than 5 minutes
const expected = crypto.createHmac('sha256', secret).update(m[1] + '.' + rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}
app.post('/msl-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verify(raw, req.get('X-MSL-Signature'), process.env.MSL_WEBHOOK_SECRET)) return res.sendStatus(401);
const event = JSON.parse(raw);
res.sendStatus(204); // confirm right away ...
handle(event); // ... and process afterwards
});PHP:
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_MSL_SIGNATURE'] ?? '';
if (!preg_match('/^t=(\d+),sha256=([a-f0-9]{64})$/', $header, $m) || abs(time() - (int) $m[1]) > 300) {
http_response_code(400);
exit;
}
$expected = hash_hmac('sha256', $m[1] . '.' . $raw, getenv('MSL_WEBHOOK_SECRET'));
if (!hash_equals($expected, $m[2])) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
http_response_code(204);Slack and Discord messages are not signed. They only contain a short summary for people, not the data above.
Delivery and retries
- Any 2xx answer counts as delivered. Answer quickly and process afterwards - without an answer within 10 seconds, the attempt counts as failed.
- We do not follow redirects (3xx). Enter the final address.
- After an error we try again after 1, 5 and 30 minutes. After the fourth failed attempt the delivery is marked as failed.
- Retries can change the order. Sort events by
createdAt, not by arrival time. - Paused or deleted webhooks receive no pending deliveries.
- The delivery list is deleted after 30 days.
Creating webhooks through the API
Besides the app, you can create and remove webhooks through the API: POST /api/v1/ext/webhooks and DELETE /api/v1/ext/webhooks/{id} with an API key that has the scope webhooks:write. This is how Zapier, n8n and Make connect without copying addresses. The fields are listed on the REST API page.
Try it with your own locations.
Create an account, import your locations, embed the map. The Free plan stays free - and you can test Pro for 14 days.