Webhooks

Instead of polling, let Helm Core OS call you. Subscribe an HTTPS endpoint to the events you care about and each one arrives signed, with automatic retries.

Set up a subscription

Webhook subscriptions are managed in Settings → API Suite → Webhooks by a user who manages the API Suite. Give it:

  • an https:// URL — internal and private addresses are refused;
  • the event types to receive;
  • optionally, custom headers, and an OAuth2 client if your endpoint wants a bearer token too (below).

You get a signing secret (whsec_…) when you create it — keep it to verify deliveries. Send test delivers a test.ping so you can check your endpoint before real events flow.

Events

EventWhen it firesPayload
user.createdA user is created.data.user
user.updatedA user's name, username, status, organisation or contact changes.data.user
user.deactivatedA user is deleted or deactivated.data.user
form.submittedA form assigned to a user is completed.data.submission
onboarding.completedA user finishes an onboarding flow (test runs excluded).data.flow, data.user, data.completed_at
deal.createdA deal is created.data.deal
deal.updatedA deal's name, status, currency, minimum investment or availability dates change.data.deal
document.uploadedA document is added to a user's or a deal's folders.data.document

Payload objects are rendered by the same resources as the API — data.deal matches a deal from GET /deals/{dealId}, and data.user matches a user as the investor and internal-user lists return it — so your existing parsing code works on both.

The delivery

Each event is POSTed to your URL as JSON:

{
  "id": "0b7e4a2c-5d1f-4c3b-9e8a-7f6d5c4b3a21",
  "type": "deal.updated",
  "created_at": "2026-06-12T10:00:00+00:00",
  "data": {
    "deal": {
      "id": "8f2c…41ad",
      "name": "Riverside Credit Fund",
      "status": "published"
    }
  }
}

with these headers:

HeaderValue
X-Delio-EventThe event type, e.g. deal.updated.
X-Delio-DeliveryA unique id for this delivery attempt.
X-Delio-Signaturesha256= + the HMAC-SHA256 of the raw body, keyed with your signing secret.
Header names

The webhook headers keep their original X-Delio-* names, so receivers that already verify them carry on working.

Respond with any 2xx within 10 seconds. Do the real work asynchronously — acknowledge first, then process — and use the event id to ignore duplicates.

Verify the signature

Always check the signature before trusting a delivery. Compute the HMAC over the raw request body — before any JSON parsing — and compare in constant time:

import { createHmac, timingSafeEqual } from 'node:crypto'

// app.post('/webhooks/helm', express.raw({ type: 'application/json' }), handler)
export function isValidSignature(rawBody: Buffer, header: string | undefined, secret: string) {
if (!header) return false
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(header)
return a.length === b.length && timingSafeEqual(a, b)
}

Retries and failures

  • A delivery that times out or gets a non-2xx is retried up to 5 attempts, after 1 minute, 10 minutes, 1 hour and 6 hours.
  • If 20 events in a row fail after all their retries, the subscription is switched off automatically. Any 2xx resets the count.
  • Re-enable a switched-off subscription in Settings; that clears the failure count.
  • Redeliver any past delivery from the delivery log. It starts a fresh set of attempts with the original event id and payload, so your duplicate check still works.

Deliveries can arrive out of order and, rarely, more than once. Use created_at to order and the event id to de-duplicate.

Authenticate to your endpoint

The signature is always sent and is how you verify Helm. If your endpoint also sits behind OAuth2, give the subscription a client id, client secret, token URL and optional scope. Helm then fetches a token with the client-credentials grant, caches it until shortly before it expires, and sends it as Authorization: Bearer … alongside the signature. The secret is encrypted at rest and never shown again.

If the token request fails, the delivery fails permanently rather than retrying, so check the token URL with Send test.

Good to know

  • Events fire for changes made through the platform and this API. A handful of older platform screens write directly to the database, and changes made there may not raise an event — for a complete mirror, pair webhooks with a periodic sync using filter[updated_at].
  • user.updated covers the user record itself; edits to a contact's first name, last name or email don't fire it.