Webhooks
Push instead of poll. We deliver signed events to URLs you control, with retries and dual-secret rotation.
Overview
- Give us an HTTPS URL on your server.
- Pick which events you want.
- We POST a JSON payload with a
Softsolz-Signatureheader. - Your server verifies the signature and returns 2xx.
- Non-2xx triggers retries with exponential backoff for up to 24 h.
Set up an endpoint
- Open the service in the dashboard → Developers → Webhooks.
- Click Add endpoint. Paste your URL. Tick events.
- Copy the signing secret shown - this is the only time you see it. Store it like a password.
Verify signatures
Header format:
Softsolz-Signature: t=1715600000,v1=<hex-hmac-sha256>
Node.js
import crypto from 'crypto'
function verify(rawBody, headerValue, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
headerValue.split(',').map(s => s.split('=').map(x => x.trim()))
)
const t = parseInt(parts.t, 10)
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex')
return crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
}
Python
import hmac, hashlib, time
def verify(raw_body, header, secret, tolerance=300):
parts = dict(p.strip().split('=', 1) for p in header.split(','))
t = int(parts['t'])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f'{t}.{raw_body}'.encode(),
hashlib.sha256).hexdigest()
return hmac.compare_digest(parts['v1'], expected)
Payload shape
{
"id": "evt_abc123",
"type": "forms.submission_received",
"created_at": "2026-05-13T17:43:12Z",
"tenant_id": "5460d86a-...",
"data": {
"submission_id": 42,
"form_id": 3,
"form_slug": "contact-us",
"values": { "name": "Jane Doe", "email": "jane@example.com" }
}
}
Event catalogue
Every event you can subscribe an endpoint to. The full per-service list is also on each service's API page.
| Event | Service | When |
|---|---|---|
forms.submission_received | Forms | Visitor or API submits a new entry. |
invoicing.invoice_paid | Invoicing | An invoice is marked or recorded as paid. |
invoicing.invoice_overdue | Invoicing | An invoice passes its due date unpaid. |
invoicing.refund_processed | Invoicing | A refund is processed against a payment. |
invoicing.payment.completed | Invoicing | A website-payment Checkout session is paid by the buyer. |
payments.payment.succeeded | Payments | A buyer completes a Checkout session or pays a payment link. |
blogs.post_published | Blogs | A post is published. |
blogs.subscriber_added | Blogs | A visitor subscribes to the blog. |
social.account_connected | Social Publishing | A social account is connected. |
social.account_disconnected | Social Publishing | A social account is disconnected. |
social.post_scheduled | Social Publishing | A post is scheduled for a future time. |
social.post_published | Social Publishing | A post is published to a target account. |
social.post_failed | Social Publishing | A target is dead-lettered after exhausting retries. |
customer-auth.user.created | Customer Auth | A new end-user registers. |
customer-auth.user.logged_in | Customer Auth | An end-user logs in. |
customer-auth.user.suspended | Customer Auth | An end-user is suspended. |
smart-chat.message.received | Knowledge Chat | The chatbot answers a message. |
smart-chat.kb.document.ready | Knowledge Chat | A Knowledge Base document finishes processing. |
workflows.run.completed | Workflows | A workflow run finishes successfully. |
workflows.run.failed | Workflows | A workflow run fails. |
invoicing.payment.completed data payload:
{
"payment_id": 8123,
"amount_cents": 4999,
"currency": "GBP",
"reference": "1042",
"customer_email": "buyer@example.com",
"stripe_session_id": "cs_live_…",
"stripe_payment_intent_id": "pi_…"
}
See the Invoicing API for how to create the Checkout session that fires this event.
payments.payment.succeeded data payload:
{
"payment_id": 8123,
"amount_cents": 4999,
"currency": "GBP",
"reference": "1042",
"customer_email": "buyer@example.com",
"stripe_session_id": "cs_live_…",
"stripe_payment_intent_id": "pi_…",
"payment_link_id": "lnk_8f3c2a1b9d"
}
payment_link_id is present only when the payment came from a payment link; it is null for server-created Checkout sessions. See the Payments API for how to create the session or link that fires this event.
Retries
- HTTP 2xx → success. We mark delivered.
- HTTP 3xx/4xx/5xx → retry on a backoff: 30 s, 5 m, 30 m, 2 h, 6 h, 12 h, 24 h.
- After 7 failed attempts, we mark the delivery permanently failed and notify the workspace admin.
Secret rotation
Click Rotate secret on the endpoint. We generate a new secret and accept signatures from both the old and new for 7 days. Update your verifier code, then the old secret expires.
Security checklist
- Always use HTTPS - we reject
http://URLs in production. - We block loopback, RFC1918 and AWS metadata IPs to prevent SSRF.
- Reject events with
|now - t| > 300sto prevent replay. - Use
timingSafeEqual/hmac.compare_digestfor signature comparison. - Idempotently apply effects keyed on
event.id- retries can deliver the same event twice.