Webhooks

Instead of polling, let Serront notify your systems. Webhooks push signed event notifications to your own HTTPS endpoint whenever something happens in your workspace — a new order, a buyer reply, a payment confirmation.

Manage endpoints at /dashboard/webhooks.

Create a subscription

In the portal, or via the API:

curl -X POST https://serront.com/api/v1/webhook-subscriptions \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/serront",
    "events": ["serront.order.created.v1", "serront.order.payment_confirmed.v1"]
  }'
  • url — your HTTPS endpoint.
  • events — optional allowlist of event types (up to 20). Omit it (or pass ["*"]) to receive everything. Entries must be "*" or a versioned serront.…vN event type.

The response includes the signing secret (whsec_…) — shown exactly once, never returned again. Store it; you need it to verify deliveries.

Subscriptions can be paused and resumed (PATCH /api/v1/webhook-subscriptions/:id with {"active": false} / {"active": true}) or deleted (DELETE /api/v1/webhook-subscriptions/:id) — all available from the portal too.

Event catalog

Event type Fires when data carries
serront.order.created.v1 A buyer submitted an order on your storefront orderId, number, serviceId, serviceSlug, packageName, quotedPriceIdr (+ discountCodeId/discountCode/discountAmountIdr when a code applied)
serront.order.replied.v1 A message was added to an order's thread orderId, messageId, by ("buyer"/"seller"), isInternal
serront.order.status_changed.v1 An order moved between statuses orderId, number, from, to
serront.order.payment_confirmed.v1 Payment settled — manual confirm or online payment orderId, number, quotedPriceIdr
serront.billing.subscribed.v1 A paid plan was activated on the workspace subscriptionId, tier, plugipayCheckoutSessionId, currentPeriodEnd

Event types are versioned (.v1); a breaking payload change ships as a new version rather than mutating the old one.

The delivery

Each delivery is an HTTP POST to your URL with a JSON body:

{
  "id": "evt_01jx2v9k3m8q4r5s6t7u8v9w0x",
  "type": "serront.order.created.v1",
  "occurredAt": "2026-06-11T03:00:00.000Z",
  "data": { "orderId": "ord_01jx…", "number": 12, "quotedPriceIdr": 1500000 }
}

id is unique per event — use it to deduplicate if your endpoint ever sees the same event twice.

Verifying signatures

Every delivery carries a Serront-Signature header:

Serront-Signature: t=1781150400,v1=5257a869e7…
  • t — unix timestamp (seconds) of when the delivery was signed,
  • v1 — hex HMAC-SHA256 of `${t}.${rawBody}` keyed with your whsec_… secret.

Recompute the HMAC over the raw request body (not a re-serialized parse of it), compare in constant time, and reject stale timestamps — 5 minutes is the tolerance Serront itself uses:

import crypto from "node:crypto";

function verifySerrontSignature(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => {
      const i = kv.indexOf("=");
      return [kv.slice(0, i), kv.slice(i + 1)];
    }),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express example — keep the raw body for verification:
app.post("/hooks/serront", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifySerrontSignature(
    process.env.SERRONT_WEBHOOK_SECRET,
    req.body.toString("utf8"),
    req.headers["serront-signature"] ?? "",
  );
  if (!ok) return res.status(401).end();
  const event = JSON.parse(req.body.toString("utf8"));
  // …handle event, respond fast:
  res.status(200).end();
});

This is the same t=…,v1=… HMAC convention used across the Forjio family (Plugipay-HMAC etc.), so existing verifier code ports over with just the header name and secret swapped.

Delivery semantics — honestly

v1 webhook delivery is fire-and-forget:

  • Events are picked up by a background worker (typically within a second or two) and POSTed to every active, matching subscription.
  • Each request has a 5-second timeout. Respond 2xx quickly and do your processing async.
  • No retries: a timeout, a non-2xx, or your endpoint being down means that delivery is gone (it's logged on our side, not re-queued). If you need certainty, treat webhooks as a hint and reconcile against GET /api/v1/orders periodically.
  • Ordering isn't guaranteed under load — use occurredAt and the event id, not arrival order.

A retry/dead-letter queue is on the roadmap; until then, build for at-most-once.

See also

  • API reference — the envelope and auth for the management endpoints.
  • Orders — the lifecycle behind the order events.