Guides: Watches and webhooks
Watches and webhooks
Watch a vehicle, and Motorbay tells your server when it changes. After each weekly import, every vehicle you watch that changed gets one signed POST to your endpoint, so you don't have to look each one up again.
Watches and webhooks need a paid plan. Every call to them is free: it counts toward the per-minute limit, but not toward your plan.
How it works
- Register an https endpoint with
POST /v1/webhooks/endpoints. Keep the signing secret from the answer: it is shown this once. - Watch vehicles with
PUT /v1/watches/vehicles/{vehicleId}. - After each weekly import, each enabled endpoint gets one
POSTper watched vehicle that changed, with the week's changes. - Check the signature of every delivery before you trust it, and answer
2xxquickly.
Register an endpoint
curl -X POST -H "X-Api-Key: $MOTORBAY_API_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/motorbay", "description": "Production"}' \
"https://api.motorbay.app/v1/webhooks/endpoints"{
"endpoint": {
"id": "12",
"url": "https://example.com/motorbay",
"description": "Production",
"enabled": true,
"disabledReason": null,
"createdAt": "2026-11-02T09:15:00Z",
"active": true
},
"secret": "whsec_KZhh+ceE0Bg1b5hNlewLsD/9F/k4bxEJD3yNJ1572UA="
}Store secret now. It can't be read again, only rotated.
The URL must be an absolute https URL on port 443, without a user name, password or fragment, and its host name must resolve only to public internet addresses. Anything else is a 400 with the code WebhookUrlNotAllowed, and detail says why.
Watch a vehicle
curl -X PUT -H "X-Api-Key: $MOTORBAY_API_KEY" \
"https://api.motorbay.app/v1/watches/vehicles/9000000006320919"Use the vehicle's id, the same one as in /v1/vehicles/{vehicleId}. Watching a vehicle you already watch changes nothing. DELETE the same path to stop: deliveries already queued for it are withdrawn, not sent. GET /v1/watches/vehicles lists your watches by vehicle id, with page and pageSize (at most 50) as in Pagination. GET /v1/webhooks/endpoints lists your endpoints, oldest first.
Watches and endpoints belong to the account: every key of the account sees and changes the same ones.
Limits
| Plan | Watched vehicles | Endpoints |
|---|---|---|
| Free | None | None |
| Starter | 150 | 5 |
| Pro | 1,000 | 5 |
| Enterprise and Business | 5,000 | 5 |
On Free, watching a vehicle or registering an endpoint is a 403 with the code PlanUpgradeRequired. Past your plan's number, it is a 409 with WatchLimitReached or EndpointLimitReached, and limit in the answer says how many your plan allows. A disabled endpoint still counts until you delete it.
After a move to a smaller plan, your watches and endpoints are kept. Your oldest ones, up to the new plan's numbers, are still delivered, and the rest are not. active on each watch and endpoint says which. Reading and removing them are always allowed, so you can tidy up.
What a delivery looks like
POST /motorbay HTTP/1.1
Host: example.com
Content-Type: application/json
webhook-id: msg_8557c51855604cef95bdf88dad46cd4c
webhook-timestamp: 1793599230
webhook-signature: v1,NaxE3fBMR1BJkCYWJY4aqHW1tM/iCKRl2u7+Y1ye5P0=
{"type":"vehicle.changed","timestamp":"2026-11-02T06:00:00Z","data":{"vehicleId":"9000000006320919","observedOn":"2026-11-02","plates":[],"statuses":[{"status":"deregistered","changedOn":"2026-10-29","observedOn":"2026-11-02"}],"registrationStatuses":[],"inspections":[]}}The example is signed with the secret above, so you can test your check against it.
typeisvehicle.changed.timestampis when Motorstyrelsen published the extract, in UTC, so every attempt sends the same body. Thewebhook-timestampheader says when this attempt was sent.data.vehicleIdis the vehicle.plates,statuses,registrationStatusesandinspectionshold only that week's changes, as the same items as in/v1/vehicles/{vehicleId}/history.webhook-idis the same on every attempt of a delivery. You may receive a delivery more than once, so use it to skip one you've already handled.- The
User-Agentstarts withMotorbay-Webhooks/.
What a change is
A delivery is what changed between two weekly extracts of Motorstyrelsen's DMR register that we loaded: a new plate, status, registration status or inspection. It is not a live view of the register. Something registered during the week arrives after the next extract and import, usually within a week, and a vehicle that changed twice in a week gets one delivery with its state at the extract. See Data source and freshness. Motorbay is not affiliated with Motorstyrelsen.
Check the signature
Deliveries are signed the Standard Webhooks way. Their libraries for most languages verify them for you, with your whsec_ secret.
To check it yourself:
- Take the bytes after
whsec_in your secret, base64 decoded. That is the key. - Sign
{webhook-id}.{webhook-timestamp}.{body}with HMAC-SHA256 and that key. Use the body's exact bytes, before you parse it. - Base64 the result. The delivery is ours if it equals one of the signatures in
webhook-signature, after thev1,of each. There can be several, separated by spaces. - Compare in constant time, and refuse a
webhook-timestampmore than five minutes from your clock.
In Node.js:
import crypto from "node:crypto";
function verify(secret, headers, rawBody) {
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const signed = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${rawBody}`;
const expected = crypto.createHmac("sha256", key).update(signed).digest();
const age = Math.abs(Date.now() / 1000 - Number(headers["webhook-timestamp"]));
return age <= 300 && headers["webhook-signature"].split(" ").some((part) => {
const given = Buffer.from(part.slice("v1,".length), "base64");
return part.startsWith("v1,") && given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}Answer, retries and 410
Answer with a 2xx within 15 seconds, before slow work: queue the delivery and process it afterwards.
Anything else is retried: a 3xx (we don't follow redirects), any 4xx but 410, a 5xx, a timeout or a refused connection. Retries are sent with the same webhook-id and body, after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours, each ±10%. After the seventh attempt the delivery is given up. A Retry-After header on your answer can make us wait longer, up to 24 hours, never shorter.
Answer 410 Gone to turn the endpoint off. Nothing more is sent to it, and GET /v1/webhooks/endpoints/{endpointId} shows "enabled": false and "disabledReason": "gone". Delete it, and register it again to start over.
For each delivery we keep the last attempt's status and up to 1 KB of your answer, for 30 days, so we can help when a delivery fails.
Rotate the secret
curl -X POST -H "X-Api-Key: $MOTORBAY_API_KEY" \
"https://api.motorbay.app/v1/webhooks/endpoints/12/rotate-secret"The answer has the new secret, shown this once. For 24 hours, each delivery is signed with both the new and the old secret, so you can switch without missing one: accept a delivery when either matches.
Errors
| Status | Code | When |
|---|---|---|
| 400 | WebhookUrlNotAllowed |
The URL can't receive deliveries. detail says why |
| 400 | ValidationFailed |
url is missing (UrlRequired in errors), or description is over 200 characters (DescriptionTooLong) |
| 403 | PlanUpgradeRequired |
Watches and endpoints need a paid plan |
| 404 | VehicleNotFound |
No vehicle with that id, when you watch it |
| 404 | WatchNotFound |
You don't watch that vehicle |
| 404 | WebhookEndpointNotFound |
You have no endpoint with that id |
| 409 | WatchLimitReached |
You watch as many vehicles as your plan allows |
| 409 | EndpointLimitReached |
You have as many endpoints as your plan allows |
| 503 | ServiceUnavailable |
We couldn't confirm your account's webhooks just now. Wait the Retry-After seconds, then retry |
See Errors for the shape of every error.
Didn't find the answer? Email support@motorbay.app.