WWaSparks Social Developers

Guides

Webhooks

Get comments, messages and publish results pushed to your server as signed events, and handle retries safely.

Subscribe

Register an https URL and the event types you want. The response contains the signing secret, once. Store it now.

curl -X POST https://connect.social.wasparks.com/v1/webhook-subscriptions \
  -H "Authorization: Bearer $WASPARKS_KEY" -H "X-Tenant-Id: acme-001" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.example.com/hooks/wasparks",
    "event_types": ["post.published", "post.failed", "comment.created", "message.received"]
  }'

The event types page lists every type and its payload.

What a delivery looks like

We POST the event to your URL as JSON. Any 2xx answer counts as delivered. We wait up to 10 seconds for it.

Headers:

Header Value
X-WSS-Event The event type, the same as the body's type.
X-WSS-Delivery This delivery's id. Retries and redeliveries keep it.
X-WSS-Signature t=<unix seconds>,v1=<hex signature>. See below.

Body, the same envelope for every event:

json
{
  "id": "evt_0192a7c4-5d00-7b88-8f99-a0b1c2d3e001",
  "type": "comment.created",
  "created_at": "2026-11-02T09:00:00Z",
  "org_id": "0192a7c1-5d2e-7a10-9c4b-2f6e8d1a3b01",
  "workspace_id": "0192a7c1-5d2e-7a10-9c4b-2f6e8d1a3b02",
  "data": { "comment": { "id": "0192a7c3-2a00-7e55-9c66-7d8e9fa0b001", "text": "Is the pumpkin latte available with oat milk?", "…": "…" } }
}

Verify the signature

Check every delivery before you trust it. The signature is an HMAC-SHA256, in hex, of the string <t>.<raw body>, keyed with the subscription's secret (the secret string as you received it, used as is):

  1. Split X-WSS-Signature into t and v1.
  2. Reject the delivery if t is more than 5 minutes from now.
  3. Compute HMAC_SHA256(secret, t + "." + raw_body) and compare it with v1 in constant time.

Use the raw bytes of the request body. If your framework parses the JSON first and you serialise it again, the bytes change and the signature will not match.

import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.WASPARKS_WEBHOOK_SECRET;

export function verifySignature(rawBody, header, secret = SECRET, toleranceSeconds = 300) {
  const parts = Object.fromEntries(String(header || "").split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  const given = Buffer.from(parts.v1 || "", "hex");
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

const app = express();
// express.raw keeps the body as bytes; do not use express.json() on this route.
app.post("/hooks/wasparks", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifySignature(req.body, req.get("X-WSS-Signature"))) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  // dedupe on event.id, then handle event.type
  res.sendStatus(200);
});

Retries and redelivery

Deliveries are at least once and not ordered. Handle both:

If your endpoint does not answer with a 2xx, we retry with growing gaps: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. After the last attempt the delivery is DEAD.

If 20 deliveries in a row end DEAD, the subscription is set to DISABLED and a webhook.disabled event goes to your other subscriptions. Fix your endpoint and turn it back on with PATCH and "status": "ACTIVE".

To see what happened to a delivery, call GET /v1/webhook-subscriptions/{subscriptionId}/deliveries. Each entry has a status (PENDING, SENDING, DELIVERED, FAILED or DEAD), the number of attempts and the last HTTP status. To send one again, call POST /v1/webhook-subscriptions/deliveries/{deliveryId}/redeliver. It carries the same event id, so your deduplication still works.

If your endpoint was down for a long time, also run POST /v1/accounts/{accountId}/comments/sync to catch up on comments.

One webhook for all your clients

If you are a partner in operator mode, you do not need one subscription per client. Create a single partner subscription:

POST /v1/partner/webhook-subscriptions
{
  "url": "https://app.example.com/hooks/wasparks",
  "event_types": ["comment.created", "message.received", "post.published", "org.created", "usage.daily_ready"]
}

It receives content events for all your orgs, together with the partner events (org.*, usage.daily_ready). Every delivery has org_id, and workspace_id for content events. Look up the client with the org's external_ref and route the event to it. Use one handler and one secret.

An org can still have its own subscription at /v1/webhook-subscriptions if one client needs a separate endpoint.

Next: Errors, limits and idempotency