// guide
Webhooks
Every change to every resource is an event. Add an endpoint, choose the events it gets, and we send each one there, signed, until your endpoint says it has it.
Endpoints
- Add endpoints in the dashboard under webhooks, or with
POST /v1/webhook_endpoints. Each mode has its own. - The URL must be HTTPS, on the public internet.
- Choose event types, or
*for all of them, including types added later. - Each endpoint has its own signing secret, starting
msk_whsec_, shown once when you make it or rotate it. Rotating stops the old one at once.
A delivery
Content-Type: application/json
User-Agent: MarketSDK-Webhooks/1
MarketSDK-Event-Id: evt_034XtPDs7UJ0TXzTsdaHZa
MarketSDK-Delivery-Id: whd_034XtPDs7UJ0TXzTsdaHZa
MarketSDK-Signature: t=1790000000,v1=5f2b...
{
"id": "evt_034XtPDs7UJ0TXzTsdaHZa",
"object": "event",
"type": "order.completed",
"api_version": "v1",
"created": "2026-10-01T12:00:00.000Z",
"livemode": false,
"data": { "object": { "object": "order", "id": "ord_...", "state": "completed" } }
}data.objectis the object as it was right after the change.- Deliveries can arrive out of order and, rarely, more than once. Use
idto skip one you have handled, and fetch the object when you need its latest state. - Every event is also in
GET /v1/events, so you can catch up after an outage.
Check the signature
The signature is an HMAC-SHA256, with your endpoint's secret, of the timestamp, a dot, and the raw body. The timestamp is signed too, so someone who captured a delivery cannot send it again later: refuse any older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
// MarketSDK-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">
export function verifyMarketSDK(header, rawBody, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}// Express: keep the body raw until it is verified.
app.post("/marketsdk/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyMarketSDK(
req.get("MarketSDK-Signature") ?? "",
req.body.toString("utf8"),
process.env.MARKETSDK_WEBHOOK_SECRET,
);
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body.toString("utf8"));
// Answer 2xx quickly, then do the work. The same event can arrive twice:
// skip an event.id you have already handled.
res.status(200).end();
queue.add(event);
});Check the raw body, byte for byte. Parsing and re-serialising the JSON first changes it, and the check fails.
Retries
An answer with a 2xx status within 10 seconds is a success. Anything else, including a redirect, is a failure, and we try again:
| attempt | when |
|---|---|
| 1 | at once |
| 2 | 1 minute after the first failure |
| 3 | 5 minutes after that |
| 4 | 25 minutes after that |
| 5 | 2 hours after that |
| 6 | 6 hours after that |
| 7 | 12 hours after that |
| 8 | 12 hours after that, more than a day after the first |
- After the eighth failure the delivery is marked
failed. The dashboard shows it with each attempt's status and response, and you can replay it. - An endpoint that has failed every delivery for 3 days is turned off, and every member of your workspace is emailed. Fix it, turn it back on, then replay what it missed.
- Replay sends the same event again as a new delivery, from the dashboard or
POST /v1/webhook_deliveries/{id}/replay.
Event types
From the OpenAPI description, so this list is always complete.
seller
- seller.created
- seller.updated
- seller.erased
- seller.suspended
- seller.unsuspended
- seller.verification_updated
- seller.payouts_updated
buyer
- buyer.created
- buyer.updated
- buyer.erased
category
- category.created
- category.updated
listing
- listing.created
- listing.updated
- listing.published
- listing.unpublished
- listing.sold
- listing.expired
- listing.removed
media
- media.ready
- media.deleted
order
- order.created
- order.committed
- order.claimed
- order.cancellation_proposed
- order.cancellation_declined
- order.cancelled
- order.expired
- order.completed
- order.disputed
payment
- payment.created
- payment.succeeded
- payment.failed
- payment.canceled
- payment.refunded
- payment.released
- payment.release_failed
refund
- refund.created
- refund.updated
verification
- verification.created
- verification.updated
review
- review.created
- review.visible
- review.removed
reputation_event
- reputation_event.created
- reputation_event.voided
dispute
- dispute.created
- dispute.evidence_added
- dispute.updated
- dispute.decision_overdue
- dispute.decided
card_dispute
- card_dispute.created
- card_dispute.updated
flag
- flag.created
- flag.resolved
moderation_action
- moderation_action.created