Developer docs
Webhooks
Signed HTTPS notifications when something changes. Proof payloads are minimal on purpose: they carry ids and a fetch URL, and the words themselves stay behind the authenticated API.
Endpoints
- Workspace owners and admins add endpoints in the workspace. Up to 20 per workspace.
- Endpoints must be public https URLs. Credentials in the URL, localhost and private network addresses are refused.
- Subscribe to exact event names, to a prefix such as proof.* or room.*, or to *.
- The signing secret (whsec_...) is shown once when you create the endpoint or rotate it. Rotation takes effect immediately; the old secret stops verifying at once, so deploy the new secret to your receiver first and expect failed deliveries in between (you can replay them).
Envelope and headers
POST /your/webhook
content-type: application/json
user-agent: YouSaid-Webhooks/1
yousaid-signature: t=1758556800,v1=5f2b...
yousaid-event-id: evt_...
yousaid-event-type: proof.consent.revoked
yousaid-delivery-id: whd_...
yousaid-delivery-attempt: 1
{ "id": "evt_...", "type": "proof.consent.revoked", "createdAt": "...", "organizationId": "...", "productId": "...", "data": { ... } }Proof events carry data with eventVersion, proofEventId, ledgerEventType, occurredAt, emittedAt, actor, productId, proofItemId, placementId, assetId, an allow-listed properties object and a fetchUrl. Customer text, reasons, evidence and personal data are not included; fetch them with a service key.
Verify the signature
v1 is the hex HMAC-SHA256 of the timestamp, a period and the raw request body, keyed with the endpoint secret. Reject timestamps more than five minutes from your clock, and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, header: string, rawBody: string, now = Date.now() / 1000) {
const t = /(?:^|,)t=(\d+)/.exec(header)?.[1];
const v1 = /(?:^|,)v1=([a-f0-9]{64})/.exec(header)?.[1];
if (!t || !v1 || Math.abs(now - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Delivery and retries
- Any 2xx response within 10 seconds counts as delivered. Redirects are not followed.
- Anything else is retried with exponential backoff: 30 seconds, doubling each time, capped at one hour, for at most 8 attempts. Then the delivery is marked failed and stays in the delivery log.
- Deliveries to a disabled endpoint fail without being sent.
- The workspace shows each endpoint's deliveries with status and payload. Replaying a delivery sends the same event id and body again as a new delivery.
Duplicates and ordering
Delivery is at least once and not ordered. Proof ledger events use evt_ plus the ledger id, which stays stable across sweeps and replays. Some proof changes are also announced directly by the request that made them, with a different envelope id, so the same change can arrive twice. Make handlers idempotent: key on the resource id in data (for example proofItemId) and re-read current state from the API before acting.
Proof events
| Type | Meaning |
|---|---|
| proof.opportunity.created | A new ask candidate appeared. |
| proof.response.submitted | A customer submitted a story or survey response (stored before any routing). |
| proof.consent.granted | A customer approved exact wording and usage. |
| proof.consent.revoked | A customer withdrew permission. Remove any copies you made outside YouSaid. |
| proof.item.approved | A teammate approved a story internally. |
| proof.asset.ready | A generated asset is ready for review. |
| proof.placement.published | A wall, widget or slot was published. |
| proof.health.warning | Proof Health found something to fix. |
| proof.brand_mention.created | Brand Monitor found a mention. |
| proof.request.opted_out | A customer opted out of story requests. Suppress them in your tools too. |
| proof.moment.detected | The Moment Engine scored a verified value moment. |
| proof.request.sent | An ask was delivered. |
| proof.response.routed | A scored response was routed. Every band is included. |
| proof.reply.captured | A customer replied to an ask by email; a draft awaits their review. |
| proof.reward.issued | A participation reward was approved and issued. |
| proof.reward.feature_unlock | A person approved a feature unlock reward. Switch on properties.unlockKey for properties.personRef, then confirm with POST /api/v1/proof/rewards/{id}/confirm. |
Direct announcements from workspace actions can also carry proof.item.imported, proof.import.completed, proof.item.rejected and proof.revocation.completed. Subscribe to proof.* to receive them.
Customer Loop events
| Type | Meaning |
|---|---|
| signal.created | A customer signal was captured. |
| thread.replied, thread.sla_escalated | A thread got a reply, or a promised update is overdue. |
| feature.voted | An end user voted on a feature. |
| release.published, release.receipt_recorded | A release shipped, or a customer confirmed its outcome. |
| room.* | Room lifecycle: created, status_changed, updated, message.created, decision.recorded, promise.approved, promise.completed, promise.missed, knowledge.published, canvas.updated, agent.action_executed, cohort.invited, release.receipt_sent. |
Inbound value events
To send events into YouSaid from Zapier, Make, n8n or a help desk without a service key, create an inbound hook in the workspace and POST to its secret URL, /api/public/proof/hooks/yshk_.... The body matches POST /api/v1/proof/events. Each eventId is stored once; a duplicate answers 200 instead of 202. A hook only proposes an ask candidate; nothing is sent to a customer without a person or an approved automation policy.