Outbound Valuation Webhooks: The Opagio Developer Guide

Antique brass balance scale on a wooden desk beside a leather-bound ledger in warm window light, one pan carrying a single calibration weight, representing the one verified delivery attempt Opagio makes for each outbound webhook event

A scheduled pull is the right way to start an integration. It is not the right way to finish one. Once your reporting job is stable, the interesting question stops being how do I get the data and becomes how do I know something changed — because polling a valuation endpoint every fifteen minutes to catch a quarterly event is an expensive way to learn nothing.

Opagio answers that with outbound webhooks: an HTTPS POST from Opagio to a URL you control, sent when a qualifying action happens inside the platform. This guide is the companion to the intangible asset valuation API developer guide, which is a prerequisite — you will need an API key and the write:webhooks scope described there before anything here is actionable.

As with that guide, every claim below is verified against the shipped delivery code rather than a roadmap. Where a property does not exist, this guide says so plainly, because a webhook integration built on an assumed guarantee fails silently and at the worst possible moment.

★ Key Takeaway

Opagio webhooks are signed with HMAC-SHA256, delivered once with a ten-second timeout, and never retried. A subscription that fails ten consecutive deliveries is automatically disabled. Design your receiver around exactly-once-attempted, not at-least-once.


The delivery contract at a glance

Property Value
Transport POST to your registered HTTPS URL
Content type application/json
Signature header X-Opagio-Signature — hex HMAC-SHA256 of the raw body
Event header X-Opagio-Event — the event name
Timeout 10 seconds
Retries None — one attempt per event
Auto-disable After 10 consecutive failures

Two design decisions sit behind the rest of this guide. First, dispatch is fire-and-forget: the platform action that triggers an event does not wait for your endpoint to answer, so a slow or broken receiver never blocks a user's request inside Opagio. Second, delivery is per-subscription and parallel, which means there is no ordering guarantee between subscriptions, and none between events either.


Step 1 — Register a subscription

Subscriptions are created through the Public API with a key holding the write:webhooks scope:

curl -s -X POST https://opag.io/api/v1/webhooks \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/opagio","events":["report.generated"]}'

The URL must begin with https:// — plain HTTP is rejected with a 400 and HTTPS URL is required. At least one event name is required. The submitted list is then filtered against the catalogue, and only recognised names are stored; if none survive the filter, the request fails with a 400 that names every valid event in the message.

That filtering is quiet rather than loud. Submit five event names of which two are misspelled, and you get a 201 with three events, not an error. The 201 response body echoes the events array that was actually stored, so compare it against what you sent rather than assuming your request was honoured verbatim.

⚠ Warning

The signing secret is a 64-character hex string returned once, in the creation response. It is stripped from every subsequent list response and there is no rotation endpoint. Store it in your secret manager at the moment of creation. To rotate it, delete the subscription and create a new one.

Subscriptions can also be managed inside the platform, under Settings → Webhooks, by a user with the SETTINGS_UPDATE permission. Both paths write the same underlying records, so a subscription created in the interface is visible and editable through the API and vice versa.


Step 2 — Verify the signature on every delivery

Each delivery carries a hex-encoded HMAC-SHA256 of the raw request body, keyed with your subscription secret, in the X-Opagio-Signature header. Recompute it and compare.

const crypto = require('crypto');

function verify(rawBody, headerSignature, secret) {
  const expected = crypto.createHmac('sha256', secret)
    .update(rawBody)          // exact bytes as received
    .digest('hex');
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(headerSignature || '', 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Two failure modes account for most broken verifications. The first is parsing the body before hashing it: a framework that runs a JSON body-parser ahead of your handler leaves you re-serialising an object, and the re-serialised bytes will not match the bytes that were signed. Capture the raw body. The second is a non-constant-time string comparison, which leaks timing information; use timingSafeEqual or your language's equivalent.

What the signature does and does not prove

Property Status
The body was produced by a holder of your subscription secret Proven
The body was not modified in transit Proven
The delivery is recent Not proven by the signature — check the body's timestamp
The delivery is not a replay of an earlier one Not proven — no nonce or delivery ID is sent

There is no delivery-ID header and no separate signed timestamp header. The envelope does carry an ISO 8601 timestamp field inside the signed body, so you can reject deliveries older than a window you choose — and because that field is inside the signed payload, it cannot be altered without invalidating the signature. That is the replay control available to you today. Build your own idempotency on top of the payload's own identifiers, such as reportId or companyId, rather than on a header Opagio does not send.


Step 3 — Know which events actually fire

The catalogue holds 26 subscribable event names. Only nine have an emitter wired behind them today. A subscription to any of the other seventeen is accepted, stored, and will never fire.

9 events with a live emitter
26 names accepted by a subscription
17 dispatch sites across the platform

The nine events that fire today

Event Fires when Payload fields
accounting.imported A general-ledger import completes companyId, valuationId, glUploadId, source
report.generated A report is created in Report Studio reportId, type, title
investor-report.generated An investor report is generated reportId, type, title
portfolio.company.created A portfolio company is added company
portfolio.company.updated A portfolio company is edited companyId, updates (changed field names)
portfolio.company.deleted A portfolio company is removed companyId
portfolio.mark.added A valuation mark is recorded companyId, mark
portfolio.bulk_import A bulk portfolio import finishes imported, failed
prediction.impairment.triggered An impairment test returns an impaired result assetId, assetName, framework, result

Note that portfolio.company.updated never sends the changed values — only the names of the fields that changed. If you need the new values, treat the event as a signal and fetch the record from GET /portfolio/companies/:id.

ℹ Note

The shorter name impairment.triggered was removed from the catalogue on 3 September 2026 — it never had an emitter, and prediction.impairment.triggered is the only name for this event.

The remaining seventeen names — every valuation.* event, portfolio.updated, company.added, company.updated, portfolio.imported, the intelligence.* family, the rest of prediction.*, the automation.* family and benchmark.dynamic.computed — are declared for future use. They are placeholders, not capabilities, and should not appear in an integration you are relying on.


Step 4 — Handle the payload envelope

Every delivery uses the same three-field envelope, regardless of event:

{
  "event": "report.generated",
  "timestamp": "2026-09-03T10:00:00.000Z",
  "data": { "reportId": "…", "type": "…", "title": "…" }
}

The event name appears in both the header and the body. Route on either, but verify the signature before you trust either — the header is not signed independently of the body, so a receiver that dispatches on X-Opagio-Event before verification is acting on unauthenticated input.

Only subscriptions that are active and whose stored events array includes the event name receive a delivery. Everything else is filtered out before any HTTP request is made.

ℹ Note

Return a 2xx status as quickly as you can, and do the work afterwards. Success is judged purely on your response status: any non-2xx response, and any connection error or timeout, counts as a failure. A receiver that performs a slow database write before responding is converting its own latency into Opagio-side failures.


Step 5 — Design for the failure behaviour

This is the section that determines whether your integration is reliable, because the failure model is stricter than most webhook implementations developers will have met.

Behaviour Detail
Timeout 10 seconds, then the attempt is abandoned
Retry None. A failed delivery is not resent
Failure counter Increments on every failed delivery
Reset A single success resets the counter to zero
Auto-disable At 10 consecutive failures the subscription is set inactive
Logging Every attempt is recorded with status, response body and outcome

Because the counter resets on success, the ten failures must be consecutive. An endpoint that fails intermittently will stay enabled indefinitely and quietly lose the events it missed. An endpoint that goes down for a sustained period will be disabled and will then miss everything until someone re-enables it — nothing re-enables it automatically.

✔ Example

A receiver behind a certificate that expires on a Friday evening fails its next ten deliveries and is disabled by Saturday. Renewing the certificate on Monday restores the endpoint but not the subscription. The subscription must be set active again through PUT /webhooks/:id, and the ten events lost over the weekend are not recoverable from the webhook path — they have to be reconciled by reading the API.

That last sentence is the whole argument for the integration pattern recommended at the end of this guide. Webhooks tell you when; a periodic read tells you what is true. An integration with only the first has no way to heal itself.

Every attempt is written to a delivery log that records the event, the payload, the HTTP status your endpoint returned, and the first 1,000 characters of your response body. That truncated body is genuinely useful when diagnosing a failing receiver — and it is a reason to keep error responses free of anything you would not want stored, since your response is retained on the Opagio side as part of the delivery audit trail.


Step 6 — Manage subscriptions over their lifetime

Operation Endpoint Scope required
List GET /webhooks read:webhooks or write:webhooks
Create POST /webhooks write:webhooks
Update PUT /webhooks/:id write:webhooks
Delete DELETE /webhooks/:id write:webhooks

The list endpoint returns your organisation's subscriptions newest first, with the secret removed from every record. A subscription belonging to a different organisation returns 404 rather than 403 on update or delete, consistent with the rest of the API: another tenant's resource is reported as absent, never as forbidden.

PUT /webhooks/:id accepts url, events and active, each optional. Sending {"active": true} is how you re-enable a subscription that auto-disabled. A url supplied here is held to the same HTTPS requirement as at creation.

ℹ Note

Event names are validated identically on both paths. At creation and on update alike, names that match no event in the catalogue are filtered out, and a request whose events array survives that filter empty is rejected with 400 rather than stored — so a typo introduced during an update is dropped, never written. The in-platform Settings → Webhooks path applies the same filter. The filter is silent about what it removed, so a partly mistyped array is accepted with the valid names only: read the subscription back with GET /webhooks after any update and confirm the events you expect are the events that are stored.


What Opagio webhooks do not do

Being precise about absence is more useful than an aspirational feature list.

  • No retries or backoff. One attempt per event, full stop.
  • No delivery ID header and no replay cache. Idempotency is yours to build, from payload identifiers.
  • No event replay or backfill endpoint. Events missed while a subscription was inactive cannot be requested again.
  • No delivery history endpoint. Attempts are logged internally but there is no API to read that log.
  • No ordering guarantee, either between subscriptions or between events.
  • No IP allow-list to pin. The signature is the authentication mechanism, not the source address.
★ Key Takeaway

Treat a webhook as a hint that something changed, and the API as the authority on what it changed to. Integrations built that way degrade gracefully when a delivery is missed. Integrations that treat the payload as the system of record do not.


A sensible first integration

Take the scheduled pull described in the Public API guide and keep it. Then add a subscription to the two or three events that genuinely matter to your workflow — for most finance teams that is report.generated, accounting.imported and, for investors running a portfolio, portfolio.mark.added.

Have the receiver do three things and nothing more: verify the signature, record that the event arrived, and trigger the same read job the scheduler already runs. The webhook becomes a latency improvement on a mechanism that already works, rather than a new dependency. If a delivery is missed, the next scheduled run closes the gap without anyone noticing.

Keep the raw signed body alongside your parsed record. When a figure in a board pack is later questioned, the ability to show exactly what arrived and when — and that it verified against the secret — is the difference between an answer and an argument.


Related reading

Share:

Ivan Gowan

Ivan Gowan — CEO, Co-Founder

25 years as tech entrepreneur, exited Angel

Connect on LinkedIn →

Related Articles

Brass balance scale on a wooden block in warm editorial light, representing the measured intangible asset valuation figures returned by the Opagio API
intangible asset valuation API 2026-09-03 · Ivan Gowan

Intangible Asset Valuation API: The Opagio Developer Guide

A developer guide to Opagio Public API v1 — the intangible asset valuation API. How to issue and authenticate an API key, what each of the four scopes grants, the two rate-limit ceilings that apply to every call, the read endpoints available today, and the conventions the API holds to.

Read more →

Subscribe to our newsletter

Get the latest insights on intangible asset growth and productivity delivered to your inbox.

Want to learn more about your intangible assets?

Take the free intangible asset assessment to see where your business stands across Opagio 12.