Webhooks let your services react to events on the Spalce platform without polling. We deliver a signed HTTPS POST to every registered endpoint whenever an event occurs — customer.created, order.completed, payment.failed, and so on. Endpoints are owned by an environment, so a sandbox subscription will only receive sandbox events.
Registering an endpoint
Endpoints are registered through the dashboard or the API. Each endpoint has a URL, a list of event types it subscribes to, and an optional description. We strongly recommend subscribing only to the events you actually need — wildcards work but cost you bandwidth and CPU you do not need to spend.
curl -X POST https://api.spalce.dev/v1/webhook_endpoints \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://acme.com/webhooks/spalce",
"events": ["customer.created", "order.completed"],
"description": "Acme order pipeline"
}'Event envelope
Every event we send shares the same envelope. The data.object field carries the resource at the moment the event was created — not the current state. If you need fresh data, fetch the resource by id, but for most workflows the snapshot is exactly what you want.
{
"id": "evt_01HEBQ4N8TZRJW2KMV7XSCYDFB",
"object": "event",
"type": "order.completed",
"created": "2026-04-30T11:08:42Z",
"livemode": true,
"data": {
"object": {
"id": "ord_01HE9X2N4MWVQRP8YZK3JTBHA7",
"object": "order",
"amount": 12000,
"currency": "GHS",
"status": "completed"
}
},
"request": { "id": "req_GH3JKL..." }
}Delivery, retries, and ordering
We expect your endpoint to respond with a 2xx status within ten seconds. Anything else — 5xx, timeout, connection refused — triggers a retry. Retries use exponential backoff with jitter, starting at 30 seconds and topping out at 6 hours. We retry for up to three days before we give up and surface the failure on the dashboard.
Webhooks are at-least-once, not exactly-once. Treat the event id as your dedupe key and persist it before you act.
Verifying signatures
Always verify the Spalce-Signature header before processing a payload. Anyone who knows your endpoint URL can send you arbitrary JSON — only a request signed with your endpoint secret should be trusted.
import crypto from "node:crypto";
export function isValid(rawBody: Buffer, header: string, secret: string) {
const fields = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(fields.t));
if (age > 300) return false;
const signed = `${fields.t}.${rawBody.toString("utf8")}`;
const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(fields.v1));
}Recommended consumer pattern
- Verify the signature on the raw request body — not the parsed JSON.
- Write the event id to a uniqueness-constrained store before doing real work.
- Acknowledge with 2xx as soon as you have durably enqueued the work.
- Process asynchronously so a slow handler cannot starve the delivery socket.
Use the dashboard's Replay feature to re-send any event from the last 30 days — handy when you are recovering from an incident on your side.
Was this article helpful?
