Webhooks
Webhooks tell your system when a record changes in Mikono. This guide lists the events, shows how to verify the signature on each delivery, and explains retries.
Add an endpoint
An administrator adds endpoints in Settings → Developers → Webhooks: a URL and the events to send to it. Mikono then shows the endpoint’s signing secret, which starts with whsec_, once. Store it with your receiver; Mikono cannot show it again.
- The URL must use HTTPS. Private and loopback addresses are refused, and the address is checked again at each send.
- Send test event delivers a
webhook.testevent, so you can check your receiver before real events arrive. - Each endpoint has a delivery log with the status, attempts, response code and duration of every delivery, and a Resend button.
What a delivery contains
Each delivery is a POST with a JSON body. data.object is the record as the API returns it, read when the event is sent, usually within seconds of the change: it can already include a later change. Use updatedAt to order. A record deleted by then arrives as { id, deleted: true, deletedAt, updatedAt }. A record too large to send whole (over 256 KB, such as a long trip’s settlement sheet) arrives as { id, truncated: true, reason, fetch, summary, updatedAt }: read it in full from the endpoint in fetch, for example GET /trips/{id}/settlement. Its summary strings are cut at 1 KB, ending with …; summaryTruncated is set when one was cut or a field left out. A stock document keeps the movements that fit and sets movementsTruncated; read the rest from GET /stock-movements.
{
"id": "evt_01J9…",
"type": "invoice.created",
"createdAt": "2026-10-02T07:15:00.000Z",
"apiVersion": "v1",
"organizationId": "…",
"data": {
"object": { "id": "…", "updatedAt": "2026-10-02T07:15:00.000Z", "…": "the invoice, as the API returns it" }
}
}Events
| Event | Sent when |
|---|---|
customer.created | An outlet is created. |
customer.updated | An outlet changes. |
product.created | A product is created. |
product.updated | A product changes. |
order.created | A sales order is created. |
order.status_changed | A sales order moves to another status. |
invoice.created | An invoice is posted. |
invoice.updated | An invoice changes, for example its balance after a payment. |
invoice.voided | An invoice is voided. |
credit_note.created | A credit note is created. |
payment.created | A collection is recorded. |
payment.reversed | A collection is reversed, for example a bounced cheque. |
stock.movement.created | Stock moves: a sale, transfer, return or adjustment. |
trip.closed | A delivery trip is closed. |
settlement.closed | A salesperson’s settlement is closed. |
dms.document.flagged | A DMS document gets a flag: received, posted, rejected or superseded. |
dms.reconciliation.completed | The daily DMS reconciliation for a salesman and day is done, or its result changed. |
dms.action.status_changed | An outbound DMS action changes status. |
webhook.test | You click Send test event. |
Verify the signature
Every delivery carries a Mikono-Signature header:
Mikono-Signature: t=1727853300,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd- Split the header on commas.
tis the time of sending in Unix seconds. Eachv1is a signature. - Compute the HMAC-SHA256 of
<t>.<raw body>with the endpoint’s secret as the key, as lowercase hex. Use the body exactly as received, before any JSON parsing. - Accept the delivery if any
v1matches, using a constant-time comparison. During a secret rotation the header carries twov1values, one per secret. - Reject the delivery if
tis more than 5 minutes from your clock. This stops an old delivery from being replayed.
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
// rawBody: the request body exactly as received (string or Buffer), before JSON parsing.
// header: the value of the Mikono-Signature header.
// secret: the endpoint's signing secret (whsec_...), used whole as the HMAC key.
export function verifyMikonoSignature(rawBody, header, secret, nowMs = Date.now()) {
let timestamp = null;
const signatures = [];
for (const part of header.split(",")) {
const [key, value] = part.trim().split("=", 2);
if (key === "t") timestamp = value;
if (key === "v1" && value) signatures.push(value);
}
if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
if (Math.abs(nowMs / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest();
return signatures.some((signature) => {
const received = Buffer.from(signature, "hex");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}Responses and retries
- A delivery succeeds when your endpoint answers any 2xx status within 10 seconds. Answer first, then process.
- Otherwise Mikono retries after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 24 hours: about two days in all. After the last attempt the delivery is marked failed.
- After 20 consecutive failed events, the endpoint is turned off with a reason, and the organisation’s administrators get a notification. Turn it on again from the Webhooks page once your receiver is fixed, and resend the failed deliveries from the delivery log.
- A failed delivery never undoes the change in Mikono that caused it.
Duplicates and order
- Delivery is at least once: the same event can arrive more than once. Store each event’s
idand skip ids you have already processed. - Events can arrive out of order. Every record carries
updatedAt; ignore a delivery whose record is older than the copy you hold.