StoreFleet can post an event to your server when something happens. Add an endpoint in Settings > API, or with POST /webhook-endpoints (permission manage:webhooks). Its signing secret, whsec_..., is shown once. Events arrive within about a minute.
| Event | When | Permission |
|---|
inquiry.created | A customer request was filed | read:inquiries |
message.sent | The store sent a text | read:messages |
message.received | A customer texted the store | read:messages |
order.paid | An order was paid | read:orders |
price_suggestion.created | A new price suggestion is waiting | read:reprices |
contact.created | A new customer was added | read:contacts |
ping | A test event you ask for from Settings or the test endpoint | none |
An endpoint only receives events its creator's permissions can read. Each event names the row it is about, and data.object has the same shape the matching endpoint answers. If the row is gone, data.object is null and data.deleted is true. Phone numbers and emails appear only when the endpoint was set to include them, which needs read:contact_info.
{
"id": "evt_5f0c2b8e9d4a4c1b8a7e6d5c4b3a2f10",
"object": "event",
"type": "inquiry.created",
"created_at": "2026-10-04T15:02:11.000Z",
"store_id": "8d3f0c3e-6c55-4d9b-9b0e-6a3f2a1b7c10",
"data": {
"object": {
"id": "2c9a4e1b-7f3d-4b5a-9e8c-1d2f3a4b5c6d",
"object": "inquiry",
"contact_id": "5b1e7c2a-3d4f-4a6b-8c9d-0e1f2a3b4c5d",
"first_name": "Sam",
"last_name": null,
"phone": null,
"email": null,
"product": "Jordan 4 Bred",
"size": "10",
"status": "open",
"kind": "source",
"source": "form",
"match_preference": "similar",
"matched_product": null,
"matched_at": null,
"notified_at": null,
"closed_at": null,
"close_reason": null,
"converted": false,
"converted_order_id": null,
"submitted_at": "2026-10-04T15:01:58.000Z",
"created_at": "2026-10-04T15:01:58.000Z"
}
}
}
Check the signature
Every delivery carries three headers, in the Standard Webhooks format: webhook-id (the event id), webhook-timestamp (unix seconds) and webhook-signature (v1, then a base64 HMAC-SHA256 of id.timestamp.body). The HMAC key is the part of your secret after whsec_, base64-decoded. Refuse any delivery more than five minutes old. Any Standard Webhooks library will do this for you; here it is in plain Node:
import crypto from "node:crypto";
// secret: the endpoint's signing secret, whsec_...
// headers: the request's headers, with lowercase names
// rawBody: the body exactly as it arrived, before any JSON parsing
export function verifyStoreFleetWebhook(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures) return false;
// Refuse a delivery more than five minutes from now.
const ts = Number(timestamp);
const now = Math.floor(Date.now() / 1000);
if (!Number.isInteger(ts) || Math.abs(now - ts) > 300) return false;
// The key is the part after whsec_, base64-decoded.
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const mac = crypto
.createHmac("sha256", key)
.update(`${id}.${ts}.${rawBody}`, "utf8")
.digest("base64");
const expected = Buffer.from(`v1,${mac}`);
// The header may carry several signatures, separated by spaces.
return signatures.split(" ").some((candidate) => {
const got = Buffer.from(candidate);
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}
Answering and retries
- Answer with any 2xx within 10 seconds. Redirects are not followed.
- Anything else is a failure. The event is tried again after waits of 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 1 day, 1 day: 9 tries in all, over about three days.
- The same event can arrive twice. Use webhook-id to skip one you already handled.
- A retried event can arrive after a newer one. Order by created_at.
- An endpoint that fails every delivery for 3 days is switched off. The owner turns it back on in Settings, or you send PATCH /webhook-endpoints/{id} with active set to true.