Skip to content

Webhooks

A webhook sends a signed event to an HTTPS endpoint of yours when something happens in Retorio, so your system does not have to poll. The event available is coaching.result.available: a coaching user’s result is ready. It carries the user, the cohort, the program, the scenario, the session times and the overall score. Its exact shape is in the callbacks of POST /organization_webhooks.

Webhooks are set up on the Integrations page of the admin app, or through the webhook endpoints, by an Owner or an admin whose role allows managing webhooks. The Admin role does not include it. The Integrations page shows the exact body and headers your endpoint receives.

Rule Value
Webhooks per organization 5, disabled ones included
URL https, at most 2048 characters, with no credentials in it
Custom headers Up to 10, for your own credential, for example a header named Authorization. Values are stored encrypted and never returned once saved. Names are returned
Header names Unique, ignoring case. Not Content-Type, Content-Length, Content-Encoding, User-Agent, Host, Expect, a hop-by-hop name such as Connection or Proxy-Authorization, and not starting with X-Retorio-
Header values At most 4096 characters of printable ASCII or tab each, and at most 8192 bytes for all custom headers together, counting each as <name>: <value> plus a line break

These are checked when the webhook is saved. A host that resolves to a private or otherwise non-routable address is accepted when saved and fails at delivery: the host is checked before every send.

  1. Accept a POST with a JSON body over HTTPS, at a URL that does not redirect. Redirects are not followed.
  2. Answer with a 2xx within 10 seconds, then do the work. A slower answer counts as a failure.
  3. Check the signature (below).
  4. Skip duplicates. Delivery is at least once. Store X-Retorio-Delivery-Id and ignore one you have already processed: a retry carries the identical body and id. Rarely, one result arrives as two events with different delivery ids. If a result must be processed only once, also skip a data.session.extId you have already seen.
Your answer What Retorio does
2xx Delivered
5xx, 408, 429, or no answer Tries again, up to 8 attempts in total spread over about a day, then gives up
Any other 4xx, or a redirect Gives up at once. Answer 4xx only for a request you will never accept

Each retry uses the webhook’s current URL, custom headers and signing secret.

Every delivery carries two headers:

Header Value
X-Retorio-Timestamp Unix seconds at send time, fresh on every attempt
X-Retorio-Signature v1, followed by a base64 HMAC-SHA256 over <timestamp>.<raw body>

To verify:

  1. Take the signing secret exactly as Retorio returned it, whsec_ prefix included, as UTF-8 bytes. Do not strip the prefix or base64-decode it.
  2. Compute the HMAC-SHA256 over the X-Retorio-Timestamp value, a dot and the raw request body, exactly as received, before any JSON parsing.
  3. Compare it with the signature using a constant-time comparison.
  4. Reject a timestamp more than a few minutes old.

The format is Retorio’s own, so verify it with your own HMAC code rather than a generic webhook verification library.

const crypto = require('crypto')
// rawBody must be the unparsed request body, exactly as it arrived.
const timestamp = request.headers['x-retorio-timestamp']
const signature = request.headers['x-retorio-signature']
const [version, digest] = typeof signature === 'string' ? signature.split(',', 2) : []
// Reject anything older than five minutes, so a captured request cannot be replayed later.
const sentAt = Number(timestamp)
const isFresh =
Number.isSafeInteger(sentAt) && Math.abs(Math.floor(Date.now() / 1000) - sentAt) <= 300
const expected = crypto
.createHmac('sha256', signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest('base64')
const received =
version === 'v1' && digest ? Buffer.from(digest, 'base64') : Buffer.alloc(0)
const computed = Buffer.from(expected, 'base64')
const isValid =
isFresh &&
received.length === computed.length &&
crypto.timingSafeEqual(received, computed)

The signing secret is shown once, when the webhook is created. Replacing it shows the new one once, and the old one stops working at that moment. Through the API, the secret is returned only by the create call and by an update that rotates it.

A result is first sent at least 5.5 minutes after it is ready, so that its session times are final, then at the next delivery run.

Action Effect
Disable Events that happen while the webhook is disabled are never sent, not even after you enable it again. Events still waiting, retries included, are called off when they come due, unless you enable the webhook before then. A called-off delivery is never resent
Delete Calls off whatever has not gone out. A request already on its way may still arrive
Send test event Sends a webhook.test event at once, even while the webhook is disabled. It is never retried

Deliveries come from no fixed IP range. Authenticate them by their signature or by a header of your own, not by source address.

Retorio is GDPR and DSGVO compliant, EU AI Act aligned, ISO 27001 certified, and hosted on Google Cloud with EU data residency. For anything specific to your rollout, ask your Retorio contact.