Webhooks

Webhooks

Subscribe to listing lifecycle events for your app. ThreadzSocial sends signed JSON POSTs to your https endpoints when API-created listings change in the matching environment (sandbox or live).

Configure in the portal

Add endpoints under My apps → your app → Webhooks. The signing secret is shown once at create time.

Events

EventWhen
listing.createdAfter a successful POST /listings create (not an external_id upsert update)
listing.updatedAfter PATCH /listings/{id} or an external_id upsert that updates an existing row
listing.archivedAfter DELETE /listings/{id} soft-archives a listing
pingManual test from the app dashboard (“Send test ping”) — always delivered to the endpoint when active

Each endpoint is scoped to sandbox or live. Only events from API keys / listings in that environment fire that endpoint. Live webhooks require a production-approved app.

Request shape

Every delivery is POST with Content-Type: application/json and a JSON body:

Envelope
{
  "id": "delivery-id-hex",
  "type": "listing.created",
  "created_at": "2026-07-18T12:00:00.000Z",
  "data": {
    "listing": {
      "id": "uuid",
      "title": "Vintage denim jacket",
      "price_cents": 4500,
      "status": "draft",
      "api_environment": "sandbox",
      "external_id": "sku-1",
      "…": "…"
    }
  }
}

Headers

HeaderValue
X-ThreadzSocial-SignatureHMAC-SHA256 hex digest of the raw request body using your endpoint secret
X-ThreadzSocial-EventEvent type (e.g. listing.created)
X-ThreadzSocial-DeliveryDelivery / event id (same as body id)
User-AgentThreadzSocial-Webhooks/1.0

Verifying signatures

Compute HMAC-SHA256(secret, rawBody) and compare the hex digest to X-ThreadzSocial-Signature with a constant-time compare. Always use the exact bytes of the body (do not re-serialize JSON before verifying).

Node.js verify example
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, signatureHex) {
  const expected = Buffer.from(
    createHmac("sha256", secret).update(rawBody, "utf8").digest("hex"),
    "hex",
  );
  const actual = Buffer.from(signatureHex.trim(), "hex");
  return (
    expected.length === actual.length &&
    expected.length > 0 &&
    timingSafeEqual(expected, actual)
  );
}

Respond quickly

Return a 2xx status within a few seconds. Deliveries time out after about 8 seconds. Failures are recorded on the endpoint (last error / last failure time) in the portal.

Testing with curl

Prefer the portal Send test ping button — it signs the payload with your real secret. To exercise your receiver locally with a known secret (replace values):

curl — local receiver smoke test
# After creating a webhook, use the revealed secret once.
# Body must match exactly what you sign.

BODY='{"id":"test","type":"ping","created_at":"2026-07-18T12:00:00.000Z","data":{"message":"local test"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "whsec_YOUR_SECRET" | awk '{print $2}')

curl -sS -X POST "https://your-receiver.example/hooks" \
  -H "Content-Type: application/json" \
  -H "X-ThreadzSocial-Event: ping" \
  -H "X-ThreadzSocial-Delivery: test" \
  -H "X-ThreadzSocial-Signature: $SIG" \
  -d "$BODY"

Listing events fire when you call the production API with your key, for example:

curl — create listing (triggers listing.created)
curl -sS -X POST https://www.threadzsocial.com/api/v1/listings \
  -H "Authorization: Bearer stz_sandbox_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Webhook test","price_cents":1200,"status":"draft"}'

Production API base

Public API base: https://www.threadzsocial.com/api/v1. Use this host for integration and go-live testing (sandbox keys are safe on the production host).

Best practices

  • Use HTTPS only; http URLs are rejected.
  • Store the signing secret in a secret manager.
  • Treat deliveries as at-least-once for now — make handlers idempotent on id / listing id.
  • Separate sandbox and live endpoints so test traffic never hits production systems.

Related