Most finance teams meet their valuation data through a screen. That works until the data needs to live somewhere else — a board pack assembled every quarter, an internal data warehouse, a lender reporting template, or an investor update that has to reconcile against the management accounts.
Opagio Public API v1 exists for that second case. It is a small, deliberately read-oriented REST surface that lets your systems pull the intangible asset valuations and company records already held in your Opagio organisation, without a person exporting anything by hand.
This guide documents what the API does today, verified against the shipped code rather than a roadmap. Where a capability does not exist, this guide says so.
★ Key Takeaway
Public API v1 is org-scoped, API-key authenticated, and read-oriented. It returns valuations and company records you already own, plus outbound webhook subscription management. It does not accept financial data for valuation, and it does not run calculations on demand.
What Public API v1 covers
| Endpoint family |
What it returns |
Scope required |
| Valuations |
Your organisation's valuation reports, list and detail |
read:valuations |
| Portfolio |
KPI summary, sector breakdown, company records |
read:portfolio |
| Webhooks |
Outbound event subscriptions — create, list, update, delete |
read:webhooks / write:webhooks |
| Widget |
A four-figure summary for an embeddable widget |
Org match on the path |
Two properties shape everything else. First, the API is org-scoped: every request is bound to exactly one organisation — the one that owns the key — and cross-tenant reads are not possible. Second, it is API-key authenticated, not session-cookie authenticated, so it is a separate path from the browser application and is not affected by anyone's login state.
| Property |
Value |
| Base URL |
https://opag.io/api/v1 |
| Auth |
Authorization: Bearer opagio_pk_… |
| Content type |
application/json |
| Versioning |
Path-based (/api/v1) plus an X-API-Version response header |
Step 1 — Issue an API key
Keys are not created through the API. They are issued inside the platform, under Settings → API keys, by a user holding the API_MANAGE_KEYS permission in an organisation that has the api-access feature flag enabled. If the flag is off for your organisation, the key-issuance page will not mint a key — speak to your account contact before you plan an integration around it.
A newly created key is granted four scopes by default: read:valuations, read:portfolio, read:webhooks and write:webhooks.
⚠ Warning
The full key is displayed exactly once, at creation. Opagio stores only a SHA-256 hash of it plus an 18-character display prefix, so the value cannot be recovered or re-shown afterwards. If you lose it, revoke the key and issue a new one.
The same page lists your existing keys by prefix and revokes them. A revoked key, or one past its expiry date, is rejected immediately on the next call.
Step 2 — Authenticate a request
Every call carries the key as a bearer token:
Authorization: Bearer opagio_pk_<64 hex characters>
The prefix and the total length are validated before any database lookup. A key is opagio_pk_ followed by 64 hexadecimal characters — 74 characters in total. Anything else is rejected on format alone, which means a truncated or padded key fails fast rather than being treated as an unknown credential.
Authentication failures and what they mean
| Condition |
Status |
Response body |
Missing or non-Bearer header |
401 |
API key required. Use Authorization: Bearer <key> |
| Wrong length or wrong prefix |
401 |
Invalid API key format. |
| No matching key |
401 |
Invalid API key. |
| Key revoked |
401 |
API key has been revoked. |
| Key expired |
401 |
API key has expired. |
These are deliberately distinguishable. A client that retries on Invalid API key format. has a bug in its configuration handling; a client that sees API key has been revoked. should stop and alert a human rather than retry.
Step 3 — Understand the four scopes
Each key carries a permissions array. Every endpoint checks the one scope it needs and returns 403 with Insufficient permissions. if it is absent.
Scope-to-endpoint mapping
| Scope |
Grants |
read:valuations |
GET /valuations, GET /valuations/:id |
read:portfolio |
GET /portfolio, GET /portfolio/companies, GET /portfolio/companies/:id |
read:webhooks |
GET /webhooks |
write:webhooks |
POST, PUT and DELETE /webhooks — and also satisfies GET /webhooks |
The GET /widget/:orgId endpoint is the exception: it checks no named scope, and instead requires the key's organisation to equal the :orgId in the path. A mismatch returns 403.
ℹ Note
If you are building a read-only reporting job, there is a real security benefit in requesting a key without write:webhooks. The default grant is convenient, not minimal, and least privilege is worth the extra conversation.
Step 4 — Budget for two rate-limit ceilings
This is the detail most integrations get wrong, because there are two independent limits and a call must satisfy both.
300
requests per 15 minutes, per source IP
300
requests per 15 minutes, per API key
429
status returned when either ceiling is hit
The per-IP ceiling is applied before authentication, as a coarse anti-abuse guard. The per-key ceiling is applied after authentication, keyed on the authenticated key, and its budget is selected from the key's rate-limit tier. The standard tier is 300 requests per 15-minute window, and an unrecognised or absent tier falls back to the same default.
The two are additive constraints, not alternatives. Spreading one key's traffic across many source addresses does not buy extra throughput, because the per-key ceiling still applies. Equally, several keys behind one office IP share the per-IP budget between them.
The two limits return different messages, which is how you tell them apart in logs:
| Ceiling hit |
Response body |
| Per-IP |
Rate limit exceeded. Please try again later. |
| Per-key |
API key rate limit exceeded. Please try again later. |
✔ Example
A nightly job that pages through 2,000 portfolio companies at the maximum 100 records per page needs 20 calls. That sits far inside both ceilings. A per-company detail fetch afterwards — 2,000 more calls — does not, and should be batched across windows or replaced by the list endpoint's payload.
Step 5 — Read valuations and company records
Valuation reports are listed newest-first and scoped to your organisation. The internal userId field is stripped from every record before it leaves the API.
curl -s https://opag.io/api/v1/valuations \
-H "Authorization: Bearer $KEY"
GET /valuations/:id returns a single report in the same shape. A report belonging to a different organisation returns 404, not 403 — the API reports another tenant's resource as absent rather than as forbidden, so existence itself is never leaked.
The read endpoints at a glance
| Endpoint |
Returns |
Fields removed before response |
GET /valuations |
Valuation reports, newest first, paginated |
userId |
GET /valuations/:id |
One valuation report |
userId |
GET /portfolio |
KPI summary plus sector breakdown |
— |
GET /portfolio/companies |
Company records, paginated, each with a derived health |
organizationId, createdBy |
GET /portfolio/companies/:id |
One company record with health |
organizationId, createdBy |
GET /widget/:orgId |
Four summary figures for an embeddable widget |
— |
The health classification on company records is derived from intangibles intensity rather than stored. For company-type organisations the records also carry a resolved sector, industryCategory and sicCode.
GET /widget/:orgId returns companyCount, totalEV, weightedAvgTFP and totalIntangibles. Where the organisation has no companies, it returns companyCount and totalEV as zero. The totalEV figure is enterprise value and weightedAvgTFP is weighted average total factor productivity.
Step 6 — Handle pagination and the response envelope
List endpoints accept page and per_page. Both are clamped server-side, so an out-of-range value is corrected rather than rejected.
| Query parameter |
Default |
Bounds |
page |
1 |
1 or greater |
per_page |
25 |
1–100, clamped |
List responses echo the values actually applied — { "data": [ … ], "page": 1, "per_page": 25 } — so a client should read the echoed values rather than assume its request was honoured verbatim. Pagination is offset-based.
Three response envelopes cover the whole surface. Reads and creates return { "data": … }. Updates and deletes return { "success": true }. Failures return { "error": "<message>" }.
Status codes you should handle
| Status |
Meaning |
| 200 |
Success |
| 201 |
Webhook subscription created |
| 400 |
Validation error in the body or parameters |
| 401 |
Authentication failure |
| 403 |
Missing scope, or widget organisation mismatch |
| 404 |
Not found — also returned for another organisation's resource |
| 429 |
Either rate-limit ceiling exceeded |
| 500 |
Server error |
Every v1 response carries an X-API-Version header of the form 1.0.<build marker>. It is set before authentication runs, so even a 401 carries it — useful when you are debugging whether a request reached the API at all.
Step 7 — Subscribe to events (briefly)
The API also manages outbound webhook subscriptions: POST /webhooks registers an HTTPS URL against a list of event names, and returns a signing secret once, in the creation response. Deliveries arrive as a POST carrying X-Opagio-Event and an X-Opagio-Signature header containing a hex HMAC-SHA256 of the raw body, keyed with that secret. Verify it against the exact raw bytes.
ℹ Note
Webhooks deserve their own treatment — the event catalogue, signature verification in practice, the ten-consecutive-failure auto-disable behaviour, and the absence of per-event retries. A dedicated companion guide covers them. Treat this section as orientation only.
One caveat worth stating now: registering an event name does not guarantee traffic. A number of names in the catalogue are declared for future use and have no emitter wired behind them yet. A subscription to one of those is accepted and will simply never fire.
What the API does not do
Being precise about absence is more useful than an aspirational feature list.
- It does not accept financial data for valuation. There is no endpoint that takes inputs and returns a computed valuation. Valuations are produced in the platform; the API reads the results.
- It does not write portfolio data. There are no create, update or delete endpoints for companies or valuations. The only writes on the surface are webhook subscriptions.
- It does not expose benchmarking or questionnaire endpoints.
- There is no published OpenAPI or Swagger schema, and no client SDKs. Any HTTP client will do; there is nothing to install.
★ Key Takeaway
Treat Public API v1 as a read replica of what you already have in Opagio, not as a valuation engine you can call. Integrations designed on that assumption tend to survive contact with the API; ones designed around an on-demand calculation endpoint do not.
A sensible first integration
A first integration that works well in practice is a scheduled pull rather than an event-driven one. Run a job on your reporting cadence, page through GET /portfolio/companies, store the response against your own record of the period, and keep the raw JSON alongside the parsed figures so that you retain an audit trail of exactly what the API returned on the day you pulled it.
That last point matters more than it sounds. Valuation figures move as underlying data is refreshed. A stored response with a timestamp is the difference between a board pack you can defend and one you can only re-run.
Once that job is stable, add a webhook subscription so the pull can be triggered by an event instead of a clock. Doing it in that order means you always have a working fallback if a delivery is missed.
Related reading