Skip to content
msMarketSDK

// 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

POST https://shop.example/marketsdk/webhookswhat we send
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.object is the object as it was right after the change.
  • Deliveries can arrive out of order and, rarely, more than once. Use id to 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.

node
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);
}
node, express
// 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:

Delivery attempts
attemptwhen
1at once
21 minute after the first failure
35 minutes after that
425 minutes after that
52 hours after that
66 hours after that
712 hours after that
812 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

next: test and live