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
| Event | When |
|---|---|
listing.created | After a successful POST /listings create (not an external_id upsert update) |
listing.updated | After PATCH /listings/{id} or an external_id upsert that updates an existing row |
listing.archived | After DELETE /listings/{id} soft-archives a listing |
ping | Manual 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:
{
"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
| Header | Value |
|---|---|
X-ThreadzSocial-Signature | HMAC-SHA256 hex digest of the raw request body using your endpoint secret |
X-ThreadzSocial-Event | Event type (e.g. listing.created) |
X-ThreadzSocial-Delivery | Delivery / event id (same as body id) |
User-Agent | ThreadzSocial-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).
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):
# 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 -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.