Webhooks

Receive real-time HTTP callbacks whenever important events occur in your SpireStock workspace — new orders, status changes, user sign-ups, and payment confirmations.

💡

Registering an endpoint

Create, update, pause, delete and test webhook endpoints in the dashboard under Developer → Webhooks, or with the API Keys & Webhooks endpoints. Registering returns a signing secret — it is shown once, and it is what the signature check below uses. If you lose it, rotate rather than re-register.

⚠️

Webhook management needs an admin session

These endpoints reject X-API-Key credentials by design, so a leaked key cannot repoint your webhooks at somebody else’s server. Manage them signed in as a workspace administrator.

How Webhooks Work

Webhooks follow a simple four-step flow:

  1. Register — You configure an HTTPS endpoint URL and choose the events you want to subscribe to via the SpireStock dashboard or API.
  2. Trigger — An event occurs in your workspace (e.g., a new order is created).
  3. Deliver — The event is queued the instant it happens and picked up by the delivery worker within 15 seconds, which POSTs the signed payload to your endpoint.
  4. Acknowledge — Your server responds 2xx within 10 seconds. Anything else is retried.
💡

Queued, not inline

Deliveries are queued durably rather than sent from inside the request that triggered them. Your endpoint being slow or down never slows down or fails the underlying operation — and because the retry schedule is stored, it survives our deploys and restarts.

⚠️

HTTPS and public addresses required

Endpoint URLs must use https:// and resolve to a public address. Plain HTTP, embedded credentials, and hosts that resolve to private, loopback, or link-local ranges are rejected — both when you register the endpoint and again at delivery time. Redirects are not followed.

Event Types

Subscribe to one or more of the following event types:

EventDescriptionFired When
order.createdA new order has been placedA sales rep or customer submits a new order through any channel
order.deliveredAn order has been deliveredThe order status changes to delivered (status 4)
user.createdA new user joined the workspaceAn admin creates a new user or a user signs up in your workspace
💡

webhook.test is not a subscription

Triggering a test sends a webhook.test event to the endpoint regardless of what it is subscribed to. You cannot subscribe to it — listing it in events is rejected — and it is never retried. It is signed exactly like a real delivery, so it is the right way to prove your signature check works.

Payload Format

Every webhook delivery includes a set of custom headers and a JSON body.

Headers

HeaderDescriptionExample
X-Webhook-EventThe event type that triggered this deliveryorder.created
X-Webhook-SignatureHMAC-SHA256 hex digest for payload verificationsha256=a1b2c3d4e5...
X-Webhook-TimestampUnix timestamp (seconds) of this attempt — a retry is re-signed with a fresh timestamp1717200000
X-Webhook-IdEvent ID, identical across retries — deduplicate on thisevt_9f3a1c7e2b8d4a6f0c5e1b7a3d9f2e4c
X-Webhook-AttemptWhich attempt this is, starting at 13
Content-TypeAlways JSONapplication/json

Example Payload

Below is a sample payload for an order.created event:

order.created payload
{
  "id": "evt_9f3a1c7e2b8d4a6f0c5e1b7a3d9f2e4c",
  "event": "order.created",
  "timestamp": "2026-06-01T10:30:00.000Z",
  "organization_id": 5,
  "data": {
    "id": 142,
    "order_code": "ORD-2026-0142",
    "order_status": 1,
    "user_id": 42,
    "user_name": "Sharma Dairy Store",
    "order_total_amount": 1400.00,
    "order_items_count": 1,
    "order_date": "2026-06-01",
    "production_unit_id": 3,
    "mode_of_payment": "credit",
    "created_at": "2026-06-01T10:30:00.000Z"
  }
}

Signature Verification

Every webhook delivery is signed using your workspace’s webhook secret. You should always verify the signature before processing a payload to ensure it was sent by SpireStock and has not been tampered with.

How It Works

  1. Concatenate the timestamp and the raw request body, separated by a dot: {timestamp}.{body}
  2. Compute an HMAC-SHA256 digest using your webhook secret as the key.
  3. Compare the computed signature with the value in the X-Webhook-Signature header using a timing-safe comparison to prevent timing attacks.
🚨

Never skip verification

Without signature verification, an attacker could forge webhook deliveries to your endpoint. Always use a timing-safe comparison function to prevent timing-based side-channel attacks.

Signature Verification
import crypto from "crypto";
import express from "express";

const app = express();

// Use raw body for signature verification
app.post(
  "/webhooks/spirestock",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-webhook-signature"];
    const timestamp = req.headers["x-webhook-timestamp"];
    const body = req.body.toString();

    // Build the signed content
    const signedContent = `${timestamp}.${body}`;

    // Compute expected signature
    const expected = "sha256=" +
      crypto
        .createHmac("sha256", process.env.WEBHOOK_SECRET)
        .update(signedContent)
        .digest("hex");

    // Timing-safe comparison
    const isValid =
      expected.length === signature.length &&
      crypto.timingSafeEqual(
        Buffer.from(expected),
        Buffer.from(signature)
      );

    if (!isValid) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    // Parse and process the event
    const event = JSON.parse(body);
    console.log("Received event:", event.event, event.id);

    // Acknowledge receipt
    res.status(200).json({ received: true });
  }
);

app.listen(3000);

Rotating the signing secret

The signing secret is returned once, when you register the endpoint, and is never included in GET /developer/webhooks. If it leaks — or you simply lost it — rotate it:

Rotate a signing secret
curl -X POST https://api.spirestock.com/api/v1/developer/webhooks/1/rotate-secret \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN"
⚠️

Rotation takes effect immediately

Deliveries are signed with the new secret from the moment you rotate, with no grace period. Deploy the new secret to your endpoint first, or accept a short window of signature failures — those deliveries will be retried, so nothing is lost.

Retries & Failure Handling

A delivery succeeds when your endpoint returns a 2xx within 10 seconds. Anything else is retried with exponential back-off: 30 seconds, then 1, 2, 4, 8, 16, 32 minutes, doubling to a 6-hour ceiling with jitter. After 12 attempts — roughly 14 hours — the delivery is marked dead and dropped.

Your responseWhat happens
2xxDelivered. The endpoint’s failure count resets to 0.
5xx, timeout, connection errorRetried — assumed temporary.
408, 429Retried — you asked us to slow down.
Any other 4xxNot retried. Your endpoint rejected the payload, and sending it again would only be rejected again.
💡

Pausing an endpoint stops the queue

Setting an endpoint’s status to 0, or deleting it, cancels its queued deliveries instead of retrying into nothing. Pause before a planned outage and no time is spent on attempts you know will fail — but events that occur while paused are not queued, and are not backfilled when you resume.

Inspecting deliveries

Every attempt is recorded — what was sent, what came back, and what is still queued. This is the first place to look when an endpoint is not receiving what you expect.

Recent deliveries for one endpoint
curl "https://api.spirestock.com/api/v1/developer/webhook-deliveries?webhook_id=1&status=dead" \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN"
Response (200 OK)
{
  "response_code": 200,
  "data": [
    {
      "id": 4821,
      "webhook_id": 1,
      "event_id": "evt_9f3a1c7e2b8d4a6f0c5e1b7a3d9f2e4c",
      "event_type": "order.created",
      "status": "dead",
      "attempts": 12,
      "max_attempts": 12,
      "last_attempt_at": "2026-08-18T04:11:07.000Z",
      "response_status": 500,
      "response_body": "Internal Server Error",
      "duration_ms": 812,
      "last_error": "Internal Server Error",
      "created_at": "2026-08-17T14:02:55.000Z"
    }
  ]
}

Filter with webhook_id, status (pending, sending, sent, dead) and limit (default 50, max 200).

Best Practices

  • Respond quickly — Return a 2xx within 10 seconds; after that the attempt is abandoned and retried. Acknowledge first, then process on your own queue.
  • Handle duplicates — Deduplicate on the event id (also sent as X-Webhook-Id), which stays the same across retries. A delivery that timed out on your side may still have been processed, so treat handlers as idempotent.
  • Validate timestamps — Reject deliveries whose X-Webhook-Timestamp is more than 5 minutes old to guard against replay. This is safe with retries: each attempt is re-signed with a fresh timestamp, so a retry arriving hours later still looks current.
  • Use a separate endpoint per environment — Register staging and production separately so each gets its own signing secret and neither can replay into the other.
  • Monitor failures — Watch failure_count on the endpoint, and read the delivery log for the response your server actually returned.
  • Do not allowlist by IP — SpireStock does not publish a fixed egress range, and it can change without notice. The signature is what proves a delivery came from us; treat it as the only check that matters.