Webhook Notifications
SSP Enterprise can deliver vault-proposal lifecycle events to your own systems as signed HTTP webhooks. Use them to drive Slack/PagerDuty alerts, kick off CI pipelines, update an internal dashboard, or trigger your own approval reminders β without polling the app.
A webhook subscription is configured per organization under Settings β Developers β Webhooks. You provide an HTTPS URL and pick which event types you care about; SSP generates a signing secret (shown once) that you use to verify every delivery is genuinely from SSP and unmodified.
Webhook payloads never contain key material. They carry only proposal metadata that is already visible to your team inside the app β vault name, chain, amount, recipient addresses, signature counts. No private keys, no signatures, no nonces, no xpubs, no witness scripts.
Event types
Each subscription is fired for the event types you select:
proposal.created
A new transaction proposal is created in a vault
proposal.needs_signature
A designated signer still needs to sign (sent to those not yet signed)
proposal.executed
The threshold was met and the transaction was broadcast
proposal.rejected
The proposal was rejected and can no longer reach its threshold
proposal.failed
Broadcast or execution failed
proposal.expired
The proposal expired before reaching its threshold
You can also scope a subscription to specific vaults, or leave it unscoped to receive events for every vault in the organization.
The event envelope
Every delivery is an HTTP POST with a JSON body in this shape:
{
"id": "evt_a1b2c3d4e5f6",
"type": "proposal.created",
"createdAt": "2026-06-23T12:00:00Z",
"organizationId": "...",
"data": {
"vaultId": "...",
"vaultName": "Treasury β BTC",
"proposalId": "...",
"chain": "btc",
"amount": "0.5 BTC",
"status": "pending_signatures",
"requiredSignatures": 2,
"currentSignatures": 0,
"recipients": [
{ "address": "bc1q...", "amount": "0.5 BTC" }
]
}
}The exact fields inside data vary slightly by event type (for example, proposal.executed includes the broadcast transaction id), but the top-level envelope β id, type, createdAt, organizationId, data β is always present.
Verifying the signature
Every request carries a signature header so you can confirm it came from SSP and was not tampered with in transit:
The v1 value is computed as:
where:
signingSecretis the secret SSP showed you once when you created the subscription,tis thet=value from theSSP-Signatureheader (Unix seconds),rawBodyis the exact raw request body bytes β verify before any JSON parsing or re-serialization, since whitespace and key order changes will break the HMAC.
To verify a request:
Parse
tandv1out of theSSP-Signatureheader.Recompute
HMAC-SHA256(signingSecret, "${t}.${rawBody}")and compare it tov1using a constant-time comparison.Reject the request if
|now - t| > 5 minutes(replay-tolerance window). This stops an attacker from re-playing an old, validly-signed request.
Node.js
Python
Use the raw body, not a re-serialized object. Frameworks that auto-parse JSON will not give you back byte-identical input. Capture the raw bytes before parsing (for example,
express.raw()in Express, orrequest.bodyin Flask) and verify against those.
Delivery, retries, and idempotency
Return
2xxquickly. SSP treats any2xxresponse as a successful delivery. Acknowledge first, then do slow work asynchronously β your endpoint should respond well within a few seconds.Retries use exponential backoff. A non-
2xxresponse, a timeout, or a connection error is retried with increasing delays. The current attempt number is in theSSP-Delivery-Attemptheader (starts at1).Deliveries are idempotent β deduplicate on
SSP-Event-Id. A retry reuses the sameSSP-Event-Id(also the top-levelidin the body). Because retries can arrive after a delivery your server actually processed (but failed to acknowledge in time), treatSSP-Event-Idas an idempotency key and ignore IDs you have already handled.Redirects are never followed. SSP posts directly to the URL you registered. A
3xxresponse is treated as a failed delivery, not a redirect to follow β register the final URL.Subscriptions auto-disable after sustained failure. If an endpoint keeps failing across many consecutive deliveries, SSP automatically disables the subscription to stop hammering a dead endpoint. You'll see it as Auto-disabled in Settings β Developers β Webhooks; fix the endpoint and re-enable it there. The per-subscription Delivery Log shows recent attempts, HTTP status, and error reason to help you debug.
Testing a subscription
After creating a subscription, use Send test in Settings β Developers β Webhooks to deliver a synthetic event to your endpoint. The test payload is signed with the same scheme as real events, so it's the quickest way to validate your verification code end to end.
Security notes
HTTPS only. Webhook URLs must be
https://. Plaintexthttp://endpoints are rejected.Keep the signing secret secret. It is shown only once at creation. If it leaks, delete the subscription and create a new one β there is no way to recover a lost secret, and rotating means creating a fresh subscription.
Always verify before trusting. Anyone who learns your URL can
POSTto it; only requests with a validSSP-Signature(and a fresh timestamp) are genuinely from SSP.Payloads are metadata-only. They contain nothing that isn't already visible to your team in the app, so a webhook endpoint never becomes a path to keys or signing material.
Next steps
Proposing & Signing Transactions β the proposal lifecycle that drives these events
Configuring Policy Controls β guardrails that may add
proposal.needs_signature(admin approval) steps
Last updated