Webhooks

Push instead of poll. We deliver signed events to URLs you control, with retries and dual-secret rotation.

Overview

  1. Give us an HTTPS URL on your server.
  2. Pick which events you want.
  3. We POST a JSON payload with a Softsolz-Signature header.
  4. Your server verifies the signature and returns 2xx.
  5. Non-2xx triggers retries with exponential backoff for up to 24 h.

Set up an endpoint

  1. Open the service in the dashboard → DevelopersWebhooks.
  2. Click Add endpoint. Paste your URL. Tick events.
  3. 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)
Always verify against the raw body bytes - not a re-serialised JSON. Re-encoding can change byte order and break the signature.

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.

EventServiceWhen
forms.submission_receivedFormsVisitor or API submits a new entry.
invoicing.invoice_paidInvoicingAn invoice is marked or recorded as paid.
invoicing.invoice_overdueInvoicingAn invoice passes its due date unpaid.
invoicing.refund_processedInvoicingA refund is processed against a payment.
invoicing.payment.completedInvoicingA website-payment Checkout session is paid by the buyer.
payments.payment.succeededPaymentsA buyer completes a Checkout session or pays a payment link.
blogs.post_publishedBlogsA post is published.
blogs.subscriber_addedBlogsA visitor subscribes to the blog.
social.account_connectedSocial PublishingA social account is connected.
social.account_disconnectedSocial PublishingA social account is disconnected.
social.post_scheduledSocial PublishingA post is scheduled for a future time.
social.post_publishedSocial PublishingA post is published to a target account.
social.post_failedSocial PublishingA target is dead-lettered after exhausting retries.
customer-auth.user.createdCustomer AuthA new end-user registers.
customer-auth.user.logged_inCustomer AuthAn end-user logs in.
customer-auth.user.suspendedCustomer AuthAn end-user is suspended.
smart-chat.message.receivedKnowledge ChatThe chatbot answers a message.
smart-chat.kb.document.readyKnowledge ChatA Knowledge Base document finishes processing.
workflows.run.completedWorkflowsA workflow run finishes successfully.
workflows.run.failedWorkflowsA 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

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