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 versionedserront.…vNevent 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 yourwhsec_…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
2xxquickly 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/ordersperiodically. - Ordering isn't guaranteed under load — use
occurredAtand the eventid, 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.