{
	"openapi": "3.0.3",
	"info": {
		"title": "Aira Connect API",
		"version": "1.0.0",
		"termsOfService": "https://aira.app/privacy-terms/general-terms-and-conditions",
		"license": {
			"name": "Proprietary. Licensed for your organization’s internal use only.",
			"url": "https://aira.app/privacy-terms/general-terms-and-conditions"
		},
		"description": "## Eligibility\n\nThis API is for your organization’s internal use. You may use Aira data inside your own business to find, research and manage companies. You may not redistribute it to third parties or expose it to users of your own product. This follows from the licences covering Aira’s company data. If your use case involves showing this data outside your organization, talk to your account manager before you build.\n\n## Authentication\n\nThis API is server-to-server only. There are two ways to authenticate, and they are alternatives rather than a migration: keys are not deprecated and nothing about them has changed.\n\n**An API key**, issued in Connect settings: `Authorization: Bearer aira_live_…`. Keys are unscoped — every key has full access to this API. Monetary exposure is capped by the organization-level spend limit, not by the key.\n\n**An OAuth 2.1 access token**, obtained through the authorization-code flow with PKCE: `Authorization: Bearer <access token>`. The customer approves the connection on a consent screen that names the organization being connected, so there is no key to find, copy or paste, and they can disconnect it again from Aira without affecting any other integration. Access obtained this way is **read-only**: every state-changing operation is refused with `capability_not_granted`, so it cannot add companies to a portfolio, submit research, or modify saved views, and therefore cannot spend the organization’s portfolio or research allowance. Reads are metered exactly as they are for an API key — see Rate limits; metering does not vary by credential. Request the `offline_access` scope or no refresh token is issued.\n\nDiscover the authorization server from `GET /.well-known/oauth-protected-resource/api/v1` (RFC 9728), or from the `WWW-Authenticate` challenge on any 401. Unlike an API key, an OAuth token is checked against the organization’s Aira Connect subscription on every request.\n\nBrowser callers are not supported: a key usable from a browser is a leaked key. No CORS headers are sent, and state-changing requests carrying an `Origin` header are rejected.\n\n## Coverage\n\nCompany data covers 14 markets: Austria (AT), Belgium (BE), Denmark (DK), Finland (FI), France (FR), Germany (DE), Ireland (IE), Italy (IT), Luxembourg (LU), Netherlands (NL), Norway (NO), Spain (ES), Sweden (SE), United Kingdom (GB). A `country` filter naming any other code matches nothing rather than failing, so check this list before you conclude a query is wrong. Individual resources cover less than the whole set and each one says so: registry filings, for example, are Sweden and Norway only.\n\n## Rate limits\n\n120 requests per minute for reads and 20 per minute for `POST /v1/portfolio/companies`. The budget is per organization rather than per key, so issuing a second key does not double it. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, so a client can pace itself instead of learning the limit by tripping it. Deep research is bounded differently: at most 5 jobs may be in flight at once, whatever rate you submit them at, so it sends no rate headers.\n\n## Envelopes and versioning\n\nCollections are cursor-paginated: pass `cursor`, read `meta.next_cursor`, and stop when it is null. The one exception is `POST /v1/portfolio/companies`, which returns one outcome per item in the request rather than a page of a larger set — so its `next_cursor` is always null and there is nothing to walk. Every successful response is wrapped in `{ data, meta }`; every error is `{ error: { code, message, details }, request_id }`, where `details` appears on the errors that have something field-specific to add.\n\nWithin `/v1` changes are additive only: fields and error codes may be added, and nothing published is removed or renamed. A breaking change would ship as a new version in the path. Timestamps are ISO-8601 UTC and numbers are unformatted.\n\n## Changelog\n\n**2026-09 — `financials[]` amounts corrected to full units.** Until this change the per-period amounts on the company record (`financials[].revenue`, `operating_profit`, `profit_before_tax`, `net_profit`, `ebitda`, `total_equity`) were returned in thousands, contrary to this specification, which has always documented them as full units. The top-level fields on the same record (`revenue`, `ebitda`, `profit`, `total_equity`) were correct throughout. If your integration read `financials[]`, values on those fields are now 1000 times larger than before and on the same scale as the top-level figures for the same year; the field names, order and types are unchanged. This is a correction to the documented contract, not a change to it. Two smaller things landed with it: `financials[].revenue` is now total operating income for the period, the same figure the top-level `revenue` derives from, where it was previously net sales; and each period gained a `currency` field carrying its own filed currency.\n\n## Error codes\n\nEvery error carries a code from the list below. The code is the part to branch on: messages are written for people and may be reworded, but a code only changes when the API version does.\n\nErrors are shaped `{ \"error\": { \"code\", \"message\", \"details\" }, \"request_id\" }`, mirroring the `{ \"data\", \"meta\" }` envelope on success, so you can tell the two apart by which top-level key is present. `request_id` is on every error and is also returned in the `X-Request-Id` header. Quote it when you contact support.\n\n**Not every code is a response status.** Some are reported inside a successful `200`: a batch add reports one outcome per company, and a research job reports why it failed in `failure.code`. The \"Where it appears\" column below says which is which: a code marked as appearing inside a `200` will never arrive as a status, so do not branch on one for it.\n\n| Code | Where it appears | Retryable |\n| --- | --- | --- |\n| `invalid_cursor` | 400 | No |\n| `invalid_id_prefix` | 400 | No |\n| `invalid_request` | 400 | No |\n| `limit_exceeds_maximum` | 400 | No |\n| `query_invalid` | 400 | No |\n| `query_too_complex` | 400 | No |\n| `saved_view_sync_too_large` | 400 | No |\n| `invalid_api_key` | 401 | No |\n| `invalid_token` | 401 | No |\n| `missing_api_key` | 401 | No |\n| `capability_not_granted` | 403 | No |\n| `company_not_in_portfolio` | 403 | No |\n| `cost_limit_reached` | In a 200, on the job | No |\n| `monitored_company_limit_reached` | In a 200, per item | No |\n| `origin_not_allowed` | 403 | No |\n| `portfolio_add_limit_reached` | In a 200, per item | No |\n| `saved_view_sync_plan_limit_reached` | 403 | No |\n| `not_found` | 404 | No |\n| `daily_row_limit_exceeded` | 429 | Yes |\n| `rate_limit_exceeded` | 429 | Yes |\n| `research_concurrency_exceeded` | 429 | Yes |\n| `internal_error` | 500 | Yes |\n| `api_unavailable` | 503 | Yes |\n| `filings_unavailable` | 503 | Yes |\n| `news_unavailable` | 503 | Yes |\n| `research_timeout` | In a 200, on the job | Yes |\n| `research_unavailable` | In a 200, on the job | Yes |\n| `saved_view_size_unavailable` | 503 | Yes |\n| `similar_companies_unavailable` | 503 | Yes |\n| `search_timeout` | 504 | Yes |\n\n### invalid_cursor\n\n**HTTP 400**\n\nThe `cursor` was not one this API issued, or is old enough that it can no longer be decoded.\n\nDiscard that cursor and start the collection again from the beginning — re-sending it returns this same error, and an error response carries no cursor to use instead. Then keep to the rule that avoids it: pass back `meta.next_cursor` exactly as you received it, and never build a cursor yourself.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### invalid_id_prefix\n\n**HTTP 400**\n\nAn id was well formed but carried the wrong resource prefix, for example a `pfe_` id where a `cmp_` id belongs.\n\nPass an id of the kind the endpoint documents. The prefix is there so you can tell at a glance which kind you are holding.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### invalid_request\n\n**HTTP 400**\n\nThe request did not match the documented schema.\n\nRead `error.details` for the offending field paths, fix them and send it again.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### limit_exceeds_maximum\n\n**HTTP 400**\n\nThe `limit` you asked for is above the maximum this endpoint accepts.\n\nLower `limit`. On a cursor-paginated collection, walk the rest of it with `meta.next_cursor`; `POST /v1/portfolio/companies` carries no cursor, so there is nothing beyond what one call returns.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### query_invalid\n\n**HTTP 400**\n\nA search query used an unknown field, an operator that field does not support, a value of the wrong type, a node with more than one key, or no query at all.\n\n`error.details` carries a JSON path to the node that failed, so you can fix one node rather than re-reading the whole tree.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### query_too_complex\n\n**HTTP 400**\n\nA search query went past a published complexity limit: nesting depth, total leaves, branches in a single `or`, or the length of an `in` list.\n\n`error.details` names the limit, its ceiling, your value and the offending node. Split the query across several calls. These limits can be raised but never lowered, so a query that passes today keeps passing.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### saved_view_sync_too_large\n\n**HTTP 400**\n\nYou replaced the query of a saved view that is synced to your CRM every night, and the new query matches more companies than a nightly sync can cover.\n\nNarrow the query — a country, an industry, or a tighter range — and send it again. Nothing was changed: the sync is still running its previous query.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### invalid_api_key\n\n**HTTP 401**\n\nThe key does not authenticate anything. Revoked, expired and never-issued keys all return this same code, so the response tells you nothing about which of the three it was.\n\nCheck you are sending the key you think you are, and that it has not been revoked. Issue a replacement in Connect settings.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### invalid_token\n\n**HTTP 401**\n\nThe OAuth access token does not authorize anything. An expired or malformed token, a token this API did not issue, a connection that has been disconnected, a membership that has lapsed, and an organization that is not on Aira Connect all return this same code, so the response tells you nothing about which of them it was.\n\nRefresh the token once. If the token was merely expired that resolves it; otherwise the refresh will succeed and the new token will be refused again, because the connection itself is no longer authorized. Do not refresh a second time — that is a loop. Reconnect the integration through the consent screen instead.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### missing_api_key\n\n**HTTP 401**\n\nThe request carried no `Authorization` header, or one this API could not parse.\n\nSend `Authorization: Bearer aira_live_…` using a key issued in Connect settings.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### capability_not_granted\n\n**HTTP 403**\n\nThe connection is read-only, or your organization does not permit connected integrations to make changes. Access obtained through the OAuth authorization-code flow is read-only by design.\n\nRetrying will not help and reconnecting will not widen the access. Use an API key issued in Connect settings for operations that change something. If you are using a key already and still see this, your organization has switched off write access and an administrator has to change that.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### company_not_in_portfolio\n\n**HTTP 403**\n\nThe company exists and you can read its summary, but it is not in your portfolio, and its contacts are only available for companies you hold.\n\nAdd the company to your portfolio with `POST /v1/portfolio/companies`, then request its contacts again. This is a separate code from an empty `data` array on purpose: an empty array means we hold no contacts for a company you do hold, which is the normal answer for most companies.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### cost_limit_reached\n\n**Reported on a research job inside a `200` response, in `failure.code`, never as a response status.**\n\nA research run stopped because your organization is at its spending limit for AI research. The submission was accepted and the job reports this as its `failure.code`, so you will see it when you poll rather than when you submit.\n\nResubmitting will fail the same way until the limit is raised. Contact your account manager.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### monitored_company_limit_reached\n\n**Reported per item inside a `200` response, in that item's `error.code`, never as a response status.**\n\nYour organization is at a monitored-company ceiling, so this company was not added. This is a **second**, separately configured limit on monitoring capacity, set on very few organizations, and it is not the plan allowance that `GET /v1/quotas` reports. Getting this code while `quotas.portfolio_companies` still has room is therefore expected rather than a contradiction.\n\nReported per company inside a 200, alongside any companies that were added. Because the ceiling is not published anywhere, there is no number to plan against: remove companies you no longer track, or contact your account manager. If you do not know whether this ceiling applies to you, it almost certainly does not.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### origin_not_allowed\n\n**HTTP 403**\n\nA state-changing request arrived with an `Origin` header, which means it came from a browser.\n\nCall this API from your server. A key that works in a browser is a key your users can read.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### portfolio_add_limit_reached\n\n**Reported per item inside a `200` response, in that item's `error.code`, never as a response status.**\n\nYour organization already holds as many companies as its plan allows, so this company was not added. The allowance counts companies **held** rather than adds performed, so re-adding one you already hold never causes this, and removing a company frees its slot immediately. It is the allowance `GET /v1/quotas` reports as `portfolio_companies`.\n\n`POST /v1/portfolio/companies` reports this per company inside a 200, so read each item rather than the response status: the rest of the batch may well have been added. `details.quotas.portfolio_companies` carries your holdings against your allowance. Remove companies you no longer follow, or contact your account manager to raise the number.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### saved_view_sync_plan_limit_reached\n\n**HTTP 403**\n\nYou replaced the query of a saved view that is synced to your CRM every night, and your plan does not have room for the companies the new query would add.\n\nRemove companies you no longer follow, or contact your account manager about a larger plan. Nothing was changed: the sync is still running its previous query.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### not_found\n\n**HTTP 404**\n\nThe resource does not exist, or is not visible to your organization.\n\nCheck the id. This API does not distinguish a resource that never existed from one you cannot see.\n\n*Retryable:* No. Retrying the same request returns the same error.\n\n### daily_row_limit_exceeded\n\n**HTTP 429**\n\nYour organization reached its daily limit for company records. The limit counts records served across every endpoint that returns companies, and it resets at midnight UTC.\n\nResume after the reset. `Retry-After` gives the seconds until then. If a scheduled import needs a bigger daily allowance, talk to your account manager rather than retrying in a loop.\n\n*Retryable:* Yes. Wait for the interval in the `Retry-After` header, then retry.\n\n### rate_limit_exceeded\n\n**HTTP 429**\n\nYour organization sent more requests per minute than this endpoint allows: 120 per minute for reads, 20 per minute for `POST /v1/portfolio/companies`. The budget is shared by every key your organization holds, so issuing a second key does not double it.\n\nWait the number of seconds in `Retry-After`, then continue. `X-RateLimit-Limit` and `X-RateLimit-Remaining` on every successful response tell you the budget and how much of the current window is left, so a client that reads them never has to reach this.\n\n*Retryable:* Yes. Wait for the interval in the `Retry-After` header, then retry.\n\n### research_concurrency_exceeded\n\n**HTTP 429**\n\nYour organization already has the maximum number of research requests running. Research is limited by how many run at once rather than by how fast you submit them, and requests started in the Aira app count towards the same ceiling.\n\nPoll the jobs you have already submitted and send the next one when one finishes. `Retry-After` is a suggested pause, not a promise that a slot will be free.\n\n*Retryable:* Yes. Wait for the interval in the `Retry-After` header, then retry.\n\n### internal_error\n\n**HTTP 500**\n\nSomething failed on our side.\n\nRetry once. If it persists, contact support and quote the `request_id` from the response body.\n\n*Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval.\n\n### api_unavailable\n\n**HTTP 503**\n\nThis API is not currently serving requests, either for your organization or for everyone. Your key is fine.\n\nNo `Retry-After` is sent, because there is no scheduled clearing time to give you. Back off and try again later, and contact support with your `request_id` if it persists.\n\n*Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval.\n\n### filings_unavailable\n\n**HTTP 503**\n\nThe filings data source did not respond in time. Your request was valid and your cursor is still good.\n\nRetry the same request with the same `cursor`. This is deliberately never returned as an empty page: an empty page with no `next_cursor` would look like the end of the feed and make a paginating client skip filings it never received.\n\n*Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval.\n\n### news_unavailable\n\n**HTTP 503**\n\nWe could not read the news corpus for your request. The upstream store timed out or answered with something we will not serve. On a single company with a lot of coverage this is a **known limitation rather than a transient fault**: the grouped query consistently runs past the read budget, so the same request returns this every time.\n\nWhere the cause is transient, wait the number of seconds in `Retry-After` and repeat the **same** request, including the same `cursor`. Do not advance your cursor: this is a refusal, not an empty page, and the articles behind it are still there — resuming from the cursor you last succeeded on loses nothing. Where a particular company returns it on every attempt, retrying will not clear it: read that company’s news through the portfolio feed instead. Where the portfolio feed itself returns it, retrying will not clear that either and there is no differently-shaped feed behind it to fall back to. An absence here is never evidence that a company has no news.\n\n*Retryable:* Yes. Wait for the interval in the `Retry-After` header, then retry.\n\n### research_timeout\n\n**Reported on a research job inside a `200` response, in `failure.code`, never as a response status.**\n\nA research run did not produce a report. A run has ten minutes to finish, and this is also the code you see when a run failed for a reason we could not classify.\n\nSubmit it again. If the same question fails repeatedly, a narrower question usually completes. Contact support with the job id if it does not.\n\n*Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval.\n\n### research_unavailable\n\n**Reported on a research job inside a `200` response, in `failure.code`, never as a response status.**\n\nA research run could not start because a service it depends on was unavailable. This is on our side and it is temporary.\n\nSubmit the same question again later. Nothing about your request needs to change.\n\n*Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval.\n\n### saved_view_size_unavailable\n\n**HTTP 503**\n\nYou replaced the query of a saved view that is synced to your CRM every night, and we could not measure how many companies the new query matches. This is on our side and it is temporary. You will also see it in the rare case where a user in the Aira app turned on a nightly sync for this view while your request was in flight, so the new query was never checked against it.\n\nSend the same request again in a moment. We will not point a nightly sync at a list we cannot measure. Nothing was changed in the meantime.\n\n*Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval.\n\n### similar_companies_unavailable\n\n**HTTP 503**\n\nThe lookalike feed could not be read. This is an upstream failure, not a statement about your data.\n\nRetry the same request, cursor and all. Do not advance your cursor and do not treat this as the end of the collection. This endpoint returns an error rather than an empty page so that the two can be told apart.\n\n*Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval.\n\n### search_timeout\n\n**HTTP 504**\n\nA search ran longer than the upstream budget allows. A broad query over a large market is the usual cause.\n\nAdd filters to narrow the query, then try again.\n\n*Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval.\n\nThis reference is generated from the error registry the API itself uses, so it cannot fall behind the implementation. New codes are additive within a version.\n\n## Testing your integration\n\nHow to build against this API without a sandbox, and what to know before your first write.\n\n### There is no sandbox\n\nEvery key is a live key, prefixed `aira_live_`, and every call runs against your real organization. What follows is how to exercise the API safely without one.\n\n### Reads are free, so start there\n\nReading never consumes export quota. `GET /v1/me` is the cheapest way to confirm a key is live: a 200 means the key authenticates, and a 401 means it is missing, revoked, expired or unknown.\n\n```bash\ncurl https://ai.aira.app/api/v1/me \\\n  -H \"Authorization: Bearer aira_live_your_key_here\"\n```\n\nCompany search and company detail are reads too. Iterate on your query shape as much as you like: the only cost is time.\n\nThe one read that is not free to explore is research: submitting a job is a write that spends money, and that has its own section below.\n\n### Research spends real money on the first call\n\n> **Warning:** `POST /v1/research` runs a real research job against your organization’s AI spending limit the first time you call it. There is no sandbox, no test key and no validation mode, so there is no way to check your integration compiles without paying for a report. Decide what you want to ask before you write the call.\n\nThree things to design around, all of them consequences of a run being expensive:\n\n- **A run takes up to ten minutes.** Poll `GET /v1/research/{id}` on the order of tens of seconds, not hundreds of milliseconds, and expect `queued` or `running` for a long time. There is no webhook in this version.\n- **Resubmitting the same question is free while the first is still running.** An identical question for the same company returns `200` with the job that already exists instead of starting a second paid run, so a client whose request timed out can send it again. Questions are compared trimmed and case-insensitively.\n- **`failed` is final.** We never retry a failed job on your behalf, so nothing arrives later to contradict what you recorded. `failure.code` tells you whether trying again could help; `cost_limit_reached` means it cannot.\n\n> **Note:** Fetch results and store what you need. We do not guarantee that a report remains retrievable indefinitely, and every company named in `mentioned_companies` counts against your daily company-record allowance each time you read the job, so poll until the status is terminal, then stop and keep the result.\n\nFive jobs may be in flight at once. `queued` and `running` both count, as does research your colleagues started in the Aira app. Over that you get `429 research_concurrency_exceeded`; `GET /v1/research` shows which jobs are holding the slots.\n\n### Adds are real\n\n> **Warning:** A company you add to your portfolio through the API is added for everyone in your organization, and if you have connected a CRM it syncs there too. There is no undo that unsends a CRM write. Test adds with companies you actually want in your portfolio.\n\nAgree internally which companies your integration tests use before you write them. A handful of well known companies you already track is the usual answer.\n\n### How quota counts\n\nQuota counts distinct companies per period, not requests. Two consequences worth designing around:\n\n- Adding the same company twice costs what adding it once costs. Re-adds are idempotent and free, so a retry after a network timeout is safe and a nightly job that re-sends yesterday's batch does not double-charge you.\n- Because it is distinct companies rather than calls, batching or splitting your requests makes no difference to what you are billed. Send them in whatever shape suits your code.\n\nRetrying a failed add is therefore always safe, which means your error handling can be simple: retry the codes marked retryable in the error codes reference, and do not build a deduplication layer of your own.\n\n### Watch your usage while you build\n\nCheck your consumption as you go rather than discovering it at the end of the period. Usage is visible in Connect settings in the Aira app, and `GET /v1/quotas` exposes the same numbers to your code.\n\n> **Note:** Call `GET /v1/quotas` before a long run rather than after it: it reports where you stand across the whole period, it costs nothing against any allowance, and it lets an automation slow down or stop on its own instead of hitting a wall mid-batch. Metered operations also report what they just spent in `meta.quotas`, in the same entry shape.\n\n### Testing the assistant surface\n\nThe MCP surface has no sandbox either, and one thing more: you are not the one deciding which calls get made. Four differences worth designing around before you connect an assistant to a live organization.\n\n- **Connect with read only, first.** Untick the write capability on the consent screen and every tool that changes something is refused with `capability_not_granted`. Nothing can then spend the portfolio allowance or the AI research budget by accident. Reads are not free of everything, though: tools that return companies still count against your organization’s daily company-record allowance, the same one the API above draws on, so an assistant left exploring can use up a real day’s worth.\n- **Allowing changes allows spending.** One capability covers portfolio adds and research submissions, so there is no setting that permits edits while forbidding a paid run. Assume the first live conversation will call the expensive tool.\n- **Every assistant sees every tool.** The tool list does not vary by what a grant allows, so a read-only connection still shows the write tools and gets refused on them. Expect `capability_not_granted` in your logs; it is the surface working, not a misconfiguration.\n- **Disconnecting is the stop button.** Anyone can disconnect their own assistants and an administrator can disconnect everyone’s, taking effect on the next request. There is no per-tool switch.\n\n> **Note:** A refusal here carries `retry_class`, which is the field to act on: it separates \"back off\", \"wait for a stated instant\", \"change the arguments\" and \"stop, a person must fix this\". A client that branches on the prose instead will eventually retry something terminal in a loop.\n\n### Call it from your server\n\nThis API is server to server. No CORS headers are sent, and a state-changing request that arrives with an `Origin` header is rejected with `origin_not_allowed`. A key that works from a browser is a key your users can read out of the page.\n\nKeys are unscoped: every key can reach everything this API exposes. Treat one like a password, store it where you store your other secrets, and rotate it in Connect settings if it is ever printed into a log.\n\n## AI assistants (MCP)\n\nThe same company data as the API above, reachable by an AI assistant instead of by your code. It is a [Model Context Protocol](https://modelcontextprotocol.io) server: an assistant that supports MCP connects to it once and can then read the organization's companies, contacts, news, filings, saved views and research — and, if the person who connected it allowed that, add companies and start research runs.\n\nIn the Aira app this surface is called [AI assistants](https://ai.aira.app/app/settings/ai-assistants). That is where a person connects an assistant and disconnects their own, and where an administrator can disconnect anybody's.\n\nThis is a different shape over the same contract, not a different product. The eligibility rules above apply unchanged: the data is licensed for your organization’s internal use, and an assistant is not a route around that.\n\n### Endpoint\n\n```text\nhttps://mcp.aira.app/mcp\n```\n\nOne URL, on its own host, with no version segment. Protocol revisions served: `2026-07-28` and `2025-11-25`. A request naming any other revision is refused with the supported list attached rather than silently downgraded.\n\nThere are no API keys here. A key belongs to your servers; an assistant is authorized per person, so that disconnecting one person does not disturb anybody else and so that every request is attributable to a human who agreed to it.\n\n### Connecting an assistant\n\nPoint the assistant at the endpoint above. Everything else is OAuth 2.1 and happens without you configuring anything:\n\n- **Discovery.** An unauthenticated request answers `401` with a `WWW-Authenticate` challenge naming [the protected-resource metadata document](https://mcp.aira.app/.well-known/oauth-protected-resource/mcp), which names the authorization server. The path-inserted form of that URL is served; the root form is not.\n- **Registration.** Clients register themselves — Dynamic Client Registration — so there is no client id to request and nothing to paste. A reinstall or a second machine registers afresh, which is why one person can legitimately hold several live connections all calling themselves the same thing.\n- **PKCE is required.** The authorization server refuses an authorization request that carries no code challenge.\n- **No scopes.** Authority lives on the grant the person approved, not in the token, so there is nothing for a scope string to express and nothing an assistant can ask for more of.\n- **The token goes in the `Authorization` header.** A token in a query string or a form body is not accepted: both put a credential somewhere it gets logged.\n\n**The consent screen appears the first time a person authorizes a given assistant.** It names the organization and asks what to allow. On later authorizations of an assistant that person has already approved, the authorization server approves it upstream before our screen can render — so do not expect to be asked again, and do not build a flow that depends on it. If an assistant reconnects but has no access to any data, remove the Aira server from it and add it again: that is what produces a fresh consent.\n\nOne grant sees one organization — the one the person who connected it belongs to. No argument switches organizations and there is no way to ask about another, so an assistant cannot be talked into looking somewhere else.\n\n### What a grant may allow\n\nTwo capabilities, `read` and `write`.\n\n- `read` is on every grant. Without it a connection authorizes nothing at all, so it is ticked and cannot be unticked.\n- `write` covers every tool that changes something: adding companies to the portfolio **and** submitting deep research.\n\n**Allowing `write` allows spending.** Adding a company consumes the portfolio allowance and, where a CRM is connected, writes to it; submitting research spends against the organization’s limit for AI work, on a run that cannot be cancelled. There is deliberately no capability that permits portfolio edits while forbidding research — one tick covers both, so treat it as authorizing money rather than authorizing edits.\n\nWhat bounds that is not a tickbox on the consent screen. An organization can forbid assistant writes outright: the capability still appears when someone connects an assistant, but it cannot be ticked and the screen says why. A monetary spending limit can also be set on the organization, and where one is set it stops research once it is reached — but it is not set by default, so do not assume a ceiling exists. Talk to your account manager if you want one before you allow assistants to spend.\n\n**Whether the assistant asks you first is your client’s decision, not ours.** We mark the two spending tools as changing something, which is the signal clients use to ask for confirmation — but some clients can be configured to run tools without asking, and in that mode a spend happens with no prompt. If you want a person to see every spend, allow `write` only to assistants whose client you know will ask. What we guarantee instead is that adding companies is bounded by the portfolio allowance you bought, and that every call is recorded against the assistant that made it. Research is bounded only if a spending limit is set — see below.\n\n**The tool list is fixed and is not filtered by capability.** Every assistant is shown all the tools, whatever its grant allows, because a list that varied by authorization would be a list that cannot safely be cached. A read-only grant therefore sees the two write tools and is refused on them with `capability_not_granted`, which is a normal outcome rather than a misconfiguration.\n\n### The tools\n\n18 tools, in the order the server lists them. Each one publishes its own argument schema, result schema and page bounds, so an assistant reads the authoritative version from the server rather than from this table. What is here is what each tool is for.\n\n| Tool | What it does | Capability | Continuation |\n| --- | --- | --- | --- |\n| `aira_me` | The organization this assistant is connected to and the person it is connected as — the only ones it can see or act as. | `read` | One record |\n| `aira_get_quotas` | How many companies the organization holds against what its plan permits. The only allowance readable before it refuses something. | `read` | One record |\n| `aira_list_companies` | The companies the organization holds, newest entry first, as full records. Entry order is when each company entered the portfolio, not when its record was created. `count_only` answers how many without reading rows, and `entered_after` returns only what has entered since a previous call. | `read` | Cursor |\n| `aira_search_companies` | Search the company universe with a boolean filter tree, or count what a filter matches without reading rows. | `read` | Cursor |\n| `aira_top_companies_by_revenue` | The largest companies by revenue matching a filter, biggest first — a ranked top slice. | `read` | Top slice, no continuation |\n| `aira_get_company` | Everything on file for one company: financials, addresses, directors, industry codes, headcount. A company the organization does not hold returns an identity summary instead. | `read` | One record |\n| `aira_get_company_group` | The corporate group a company belongs to — its parent and every company under that parent — for any company, held or not. `meta.incomplete` says when members are missing: the group exceeded the cap, or a member carried a kind we do not recognise. Branch offices are left out by design and are not reported that way. | `read` | Whole set up to a cap |\n| `aira_list_company_contacts` | The people on file for a company in the portfolio: name, title, email, phone, sometimes a LinkedIn URL. | `read` | Cursor |\n| `aira_list_news` | News across the portfolio, newest first, grouped so one event is one item rather than one article per outlet. | `read` | Cursor |\n| `aira_list_company_news` | The same news feed narrowed to one company in the portfolio. | `read` | Cursor |\n| `aira_list_filings` | Business-registry changes across the portfolio, newest first. Sweden and Norway only. | `read` | Cursor |\n| `aira_list_company_filings` | The same registry feed narrowed to one company in the portfolio. | `read` | Cursor |\n| `aira_find_similar_companies` | Companies resembling ones already held, each row naming the portfolio company it was drawn from and why. | `read` | Cursor |\n| `aira_list_saved_views` | The organization’s saved views — named, stored company queries. Read-only here: no tool creates, renames, changes or deletes one. | `read` | Cursor |\n| `aira_get_saved_view_companies` | Runs a saved view’s stored query and returns what matches now. Membership is live rather than synced. | `read` | Cursor |\n| `aira_add_portfolio_companies` | Adds companies to the portfolio by id and reports what happened to each distinct id. Spends the portfolio allowance, and syncs to a connected CRM. | `write` | One per unique id |\n| `aira_submit_research` | Starts a deep research run against a company the organization holds. Spends real money on its AI budget, with no dry run and no cancel. | `write` | One record |\n| `aira_get_research` | Reads a research job back and, once the run has finished, its report. | `read` | One record |\n\nThe continuation column is the part most likely to be assumed wrong:\n\n- **Cursor** — a page of a larger set. `meta.next_cursor` carries an opaque cursor; echo it back, and stop when it is null.\n- **One per unique id** — the response reports an outcome for each distinct id in the request, so there is nothing to continue. Repeated ids collapse before anything is processed, so match outcomes on `company_id` rather than on position: a request with duplicates gets back fewer rows than it sent. `meta.next_cursor` is published and is always null.\n- **Whole set up to a cap** — no page argument, so one call returns the whole set and there is no way to ask for more of it. `meta.incomplete` is true when members are missing: either the group was large enough to exceed the cap, or a member carried a kind we do not recognise and was left out rather than guessed at. Read that flag before describing a group as complete; a false value is the only thing that means you have all of it. Branch offices are a separate matter — they are excluded deliberately, because a branch is a location rather than a company, and their absence is not reported by that flag.\n- **Top slice, no continuation** — a bounded window on something larger that cannot be walked. No cursor argument, and **no `next_cursor` in the result at all**: a null cursor would claim you had reached the end, which would be false. `meta.incomplete` says more matched than came back, and narrowing the filter is the only way further down.\n\nNews and filings are read as feeds — filtered by company, date, language and filing type — and there is no text query over them. Registry filings cover Sweden and Norway only, and an empty answer for a company elsewhere is a coverage gap rather than a quiet company. No tool takes free text at all: the lookalike feed finds companies resembling ones already held, and there is no way to search the whole corpus by description or meaning.\n\nSaved views are readable and not writable: no tool creates, renames, changes or deletes one. A saved view can carry a nightly CRM sync that nobody in a conversation would connect to the words \"clear that out\", so the destructive half of that resource is deliberately absent.\n\n### Results\n\nEvery successful result is `{ \"data\", \"meta\", \"metering\" }`. The rows are in `data`; `meta` carries the cursor where there is one, plus whatever else the tool declares. The same payload is returned in both the text and structured channels of the tool result, so it does not matter which one a client reads.\n\nAn absent field means we hold no value for it. It does not mean zero, and reporting an absence as a fact about a company is the most common way to be wrong with this data.\n\nAllowances and rate limits are the organization’s, shared with the API above rather than counted separately: the same portfolio allowance, the same daily company-record ceiling, the same research concurrency limit.\n\nReads are burst-bounded twice: once for the person who connected the assistant — every assistant that person connected shares that budget — and once for the organization, which everyone in it shares. A refusal names which of the two ran out in `bucket` and carries the budget itself in `limit_per_minute`. The organization's read budget is 120 requests a minute, the same figure the API above publishes, and one person may take at most half of it, so two colleagues can always work at once.\n\n### Refusals and error codes\n\nEvery refusal carries a code from the list below. The code is the part to branch on: messages are written for people and may be reworded, but a code only changes when the contract does.\n\nA refusal arrives as a **tool result marked as an error** rather than as a JSON-RPC error, so it reaches the model and can carry typed fields. Its shape is `{ \"error\": { \"code\", \"message\", \"retry_class\", \"retry_at\"?, \"bucket\"?, \"limit_per_minute\"?, \"details\"? }, \"request_id\" }`.\n\n**`retry_class` is the field to act on**, not the prose. It has four values — `retry_after_window`, `not_until_reset`, `fix_and_retry` and `human_must_act` — and they are the difference between backing off, waiting for a stated instant, changing the arguments, and stopping because only a person can clear it. A stateless server has no session in which to notice a retry storm, so the instruction travels in the refusal itself.\n\n**Not every code is a refusal.** Some are reported inside a result that otherwise succeeded: an add reports one outcome per company, and a research job records why it failed in `failure.code`. The \"Where it appears\" column says which is which.\n\nNumbers never appear in a refusal message. Where a figure is publishable it travels as a typed field — `limit_per_minute` and `retry_at` on a burst refusal — and where it is not, it appears nowhere at all.\n\n| Code | Where it appears | `retry_class` |\n| --- | --- | --- |\n| `api_unavailable` | Tool result marked as an error | `retry_after_window` |\n| `capability_not_granted` | Tool result marked as an error | `human_must_act` |\n| `company_not_in_portfolio` | Tool result marked as an error | `fix_and_retry` |\n| `cost_limit_reached` | On a research job | `human_must_act` |\n| `daily_row_limit_exceeded` | Tool result marked as an error | `not_until_reset` |\n| `filings_unavailable` | Tool result marked as an error | `retry_after_window` |\n| `internal_error` | Tool result marked as an error | `retry_after_window` |\n| `invalid_cursor` | Tool result marked as an error | `fix_and_retry` |\n| `invalid_id_prefix` | Tool result marked as an error | `fix_and_retry` |\n| `invalid_request` | Tool result marked as an error | `fix_and_retry` |\n| `limit_exceeds_maximum` | Tool result marked as an error | `fix_and_retry` |\n| `monitored_company_limit_reached` | In a result, per company | `human_must_act` |\n| `news_unavailable` | Tool result marked as an error | `retry_after_window` |\n| `not_found` | Tool result marked as an error | `fix_and_retry` |\n| `portfolio_add_limit_reached` | In a result, per company | `human_must_act` |\n| `query_invalid` | Tool result marked as an error | `fix_and_retry` |\n| `query_too_complex` | Tool result marked as an error | `fix_and_retry` |\n| `rate_limit_exceeded` | Tool result marked as an error | `retry_after_window` |\n| `research_concurrency_exceeded` | Tool result marked as an error | `retry_after_window` |\n| `research_timeout` | On a research job | `retry_after_window` |\n| `research_unavailable` | On a research job | `retry_after_window` |\n| `search_timeout` | Tool result marked as an error | `fix_and_retry` |\n| `similar_companies_unavailable` | Tool result marked as an error | `retry_after_window` |\n\n#### api_unavailable\n\n**Delivered as a tool result marked as an error.**\n\nThis surface is not currently serving requests, either for this organization or for everyone. The credential is fine — a switch is off.\n\nNo `retry_at` is sent, because a human flips this switch and there is no clearing time to state. Back off and try later rather than immediately, and tell the user if it persists.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\n#### capability_not_granted\n\n**Delivered as a tool result marked as an error.**\n\nThe assistant is connected with a grant that does not include this capability, or your organization does not permit assistants to make changes at all.\n\nRetrying will not help and no assistant can widen its own access. Open [AI assistants](https://ai.aira.app/app/settings/ai-assistants) in your Connect settings and reconnect with the capability ticked. If the capability is not offered there, your organization has switched off write access for assistants and an administrator has to change that.\n\n*`retry_class`:* `human_must_act` — Stop. Only a person can clear this.\n\n#### company_not_in_portfolio\n\n**Delivered as a tool result marked as an error.**\n\nThe company exists and its summary is readable, but the organization does not hold it, and contacts are only available for companies it holds.\n\nAdding the company opens this — which is a spend, and needs the user’s agreement. This is a separate code from an empty list on purpose: an empty list means we hold no contacts for a company the organization does hold, which is the normal answer for most companies.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### cost_limit_reached\n\n**Reported on a research job in `failure.code`, never as a refused call.**\n\nThe run stopped because the organization is at its spending limit for AI work. The submission was accepted, so this arrives on the job as its `failure.code` when it is read back — never as a refusal of the submission.\n\nResubmitting fails the same way until someone raises the limit, which is why this is terminal. Tell the user their account manager can raise it. The limit itself is never published, so there is no figure to report.\n\n*`retry_class`:* `human_must_act` — Stop. Only a person can clear this.\n\n#### daily_row_limit_exceeded\n\n**Delivered as a tool result marked as an error.**\n\nThe organization reached its daily limit for company records. The limit counts records served across every tool that returns companies, and it resets at midnight UTC.\n\n`retry_at` is the exact reset instant, and nothing before it clears this — that is what `retry_class: not_until_reset` means. The ceiling itself is not published, on this surface or any other, so there is no figure to pace against; tell the user, and talk to the account manager if a routine question keeps landing here.\n\n*`retry_class`:* `not_until_reset` — Nothing clears this before the stated instant.\n\n#### filings_unavailable\n\n**Delivered as a tool result marked as an error.**\n\nThe filings data source did not respond in time. Your request was valid and your cursor is still good.\n\nRetry the same request with the same `cursor`. This is deliberately never returned as an empty page: an empty page with no `next_cursor` would look like the end of the feed and make a paginating client skip filings it never received.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\n#### internal_error\n\n**Delivered as a tool result marked as an error.**\n\nSomething failed on our side.\n\nCalling once more is reasonable. If it persists, tell the user rather than looping — the `request_id` on the refusal is what a support conversation is looked up by.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\n#### invalid_cursor\n\n**Delivered as a tool result marked as an error.**\n\nThe `cursor` was not one this API issued, or is old enough that it can no longer be decoded.\n\nDiscard that cursor and start the collection again from the beginning — calling with it again returns this same error, and a refusal carries no cursor to use instead. Then keep to the rule that avoids it: pass back `meta.next_cursor` exactly as it arrived, never build a cursor, and never reuse one from a different question.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### invalid_id_prefix\n\n**Delivered as a tool result marked as an error.**\n\nAn id was well formed but carried the wrong resource prefix, for example a `pfe_` id where a `cmp_` id belongs.\n\nPass an id of the kind the tool documents. The prefix is there so you can tell at a glance which kind you are holding, and ids only ever come from an earlier result.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### invalid_request\n\n**Delivered as a tool result marked as an error.**\n\nThe arguments did not match the tool’s input schema.\n\n`details` names the arguments that were wrong, one entry per offending path. Correct them and call the tool again.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### limit_exceeds_maximum\n\n**Delivered as a tool result marked as an error.**\n\nThe page size asked for is above the maximum this tool accepts.\n\n`details` carries the maximum and what was requested. Ask for that many or fewer. Where the tool publishes a cursor, take the rest of the collection with `meta.next_cursor`; the ranked slice publishes none, and narrowing the filter is the only way further down it.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### monitored_company_limit_reached\n\n**Reported per company inside a result that otherwise succeeded, in that entry's own `error.code`, never as a refused call.**\n\nThe organization is at a monitored-company ceiling, so this company was not added. This is a **second**, separately configured limit on monitoring capacity, set on very few organizations, and it is not the plan allowance `aira_get_quotas` reports. Getting this while `portfolio_companies` still has room is therefore expected rather than a contradiction.\n\nReported per company on an otherwise successful result, alongside any companies that were added. The ceiling is not published anywhere, so there is no number to plan against: remove companies the organization no longer tracks, or contact the account manager.\n\n*`retry_class`:* `human_must_act` — Stop. Only a person can clear this.\n\n#### news_unavailable\n\n**Delivered as a tool result marked as an error.**\n\nThe news store could not be read. On a single company with a lot of coverage this is a **known limitation rather than a transient fault**: the grouped query consistently runs past the read budget, so the same call returns this every time.\n\nWhere the cause is transient, repeat the same call — cursor and all — at `retry_at`, and do not advance the cursor: the articles behind this are still there. **Where one company returns this twice, stop retrying it** — `retry_class` cannot express that exception, and further attempts fail identically. Read that company’s news from `aira_list_news` instead. **Where `aira_list_news` itself returns this, do not count on a retry either**, and note that there is no differently-shaped feed behind it: report that the feed could not be read rather than looking for another route to it. Never report the absence as the company having no news.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\n#### not_found\n\n**Delivered as a tool result marked as an error.**\n\nThe resource does not exist, or is not visible to your organization.\n\nCheck the id. This API does not distinguish a resource that never existed from one you cannot see.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### portfolio_add_limit_reached\n\n**Reported per company inside a result that otherwise succeeded, in that entry's own `error.code`, never as a refused call.**\n\nThe organization already holds as many companies as its plan allows, so this company was not added. The allowance counts companies **held** rather than adds performed, so re-adding one already held never causes this, and removing a company frees its slot immediately. It is the allowance `aira_get_quotas` reports as `portfolio_companies`.\n\nThe add reports this per company on an otherwise successful result, so read every entry: the rest of the batch may well have been added. Read the allowance with `aira_get_quotas` before a large add. Removing companies the organization no longer follows frees slots; otherwise the account manager raises the number.\n\n*`retry_class`:* `human_must_act` — Stop. Only a person can clear this.\n\n#### query_invalid\n\n**Delivered as a tool result marked as an error.**\n\nA search query used an unknown field, an operator that field does not support, a value of the wrong type, a node with more than one key, or no query at all.\n\n`details` carries a JSON path to the node that failed, so you can fix one node rather than rebuilding the whole filter.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### query_too_complex\n\n**Delivered as a tool result marked as an error.**\n\nA search query went past a published complexity limit: nesting depth, total leaves, branches in a single `or`, or the length of an `in` list.\n\n`details` names the limit, its ceiling, the value sent and the offending node. Split the question across several calls. These limits can be raised but never lowered, so a filter that passes today keeps passing.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### rate_limit_exceeded\n\n**Delivered as a tool result marked as an error.**\n\nToo many calls in the current minute. Two budgets can produce this: the one belonging to the person who connected this assistant, shared by every assistant they connected, and the organization’s, shared by everyone in it. `bucket` says which one refused — `user` for the person, `organization` for the organization.\n\n`limit_per_minute` is the budget that refused the call and `retry_at` is when it refills, so pace against those rather than guessing. The two clear on the same cadence for different reasons: the organization’s can be exhausted by a colleague rather than by anything this assistant did.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\n#### research_concurrency_exceeded\n\n**Delivered as a tool result marked as an error.**\n\nThe organization already has as many research jobs in progress as may run at once, counting jobs started in the Aira app. Both queued and running jobs occupy a slot.\n\nA slot frees when a job finishes rather than after a fixed wait, which is why this refusal carries no `retry_at`. If this assistant started one of the runs, read it back by its id and send the next question once it has stopped. If it did not, there is nothing here to poll — the slots belong to runs started elsewhere, and the only options are to wait or to tell the user.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\n#### research_timeout\n\n**Reported on a research job in `failure.code`, never as a refused call.**\n\nA research run did not produce a report. A run has ten minutes to finish, and this is also the code you see when a run failed for a reason we could not classify.\n\nSubmitting the same question again is safe, and a narrower question usually completes where a broad one did not. If it keeps failing, stop resubmitting — each attempt spends — and tell the user, with the job id so a person can follow it up.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\n#### research_unavailable\n\n**Reported on a research job in `failure.code`, never as a refused call.**\n\nA research run could not start because a service it depends on was unavailable. This is on our side and it is temporary.\n\nSubmit the same question again later. Nothing about your request needs to change.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\n#### search_timeout\n\n**Delivered as a tool result marked as an error.**\n\nA search ran longer than the upstream budget allows. A broad query over a large market is the usual cause.\n\nAdd filters to narrow the query, then try again.\n\n*`retry_class`:* `fix_and_retry` — The same call always fails; a different one may work.\n\n#### similar_companies_unavailable\n\n**Delivered as a tool result marked as an error.**\n\nThe lookalike feed could not be read. This is an upstream failure, not a statement about your data.\n\nRepeat the same call, cursor and all. Do not advance the cursor and do not treat this as the end of the collection: for this feed an empty page is an ordinary answer, which is exactly why a failure is reported as an error instead.\n\n*`retry_class`:* `retry_after_window` — Back off and send the same call again.\n\nThis reference is generated from the error registry the API itself uses, so it cannot fall behind the implementation. New codes are additive within a version."
	},
	"servers": [
		{
			"url": "https://ai.aira.app/api",
			"description": "Production"
		}
	],
	"security": [
		{
			"ApiKeyAuth": []
		},
		{
			"OAuth2": []
		}
	],
	"components": {
		"securitySchemes": {
			"ApiKeyAuth": {
				"type": "http",
				"scheme": "bearer",
				"description": "An API key issued in Connect settings, prefixed `aira_live_`. Revoked, expired and unknown keys are indistinguishable: all return 401 `invalid_api_key`."
			},
			"OAuth2": {
				"type": "oauth2",
				"description": "An OAuth 2.1 access token obtained through the authorization-code flow with PKCE. The customer approves the connection on a consent screen that names the organization being connected; the resulting access is read-only, and every state-changing operation is refused with `capability_not_granted`. Reads are metered exactly as they are for an API key; metering does not vary by credential. Request the `offline_access` scope or no refresh token is issued. Every refusal — an expired or forged token, a disconnected integration, a lapsed membership, an organization not on Aira Connect — returns the same 401 `invalid_token`, so the response tells you nothing about which it was.",
				"flows": {
					"authorizationCode": {
						"authorizationUrl": "https://jgtojkxesipymvwwsnpi.supabase.co/auth/v1/oauth/authorize",
						"tokenUrl": "https://jgtojkxesipymvwwsnpi.supabase.co/auth/v1/oauth/token",
						"refreshUrl": "https://jgtojkxesipymvwwsnpi.supabase.co/auth/v1/oauth/token",
						"scopes": {
							"offline_access": "Issue a refresh token, so the connection survives the first access-token expiry."
						}
					}
				}
			}
		},
		"schemas": {
			"CompanyQuery": {
				"id": "CompanyQuery",
				"description": "A boolean query tree. Every node is either a predicate (`field`, `operator`, `value`) or exactly one of `and`, `or` and `not`. A node carrying two of those shapes is rejected rather than resolved by precedence.",
				"oneOf": [
					{
						"type": "object",
						"properties": {
							"field": {
								"type": "string",
								"description": "The company attribute to filter on.",
								"enum": [
									"cash_and_bank",
									"city",
									"company_form",
									"country",
									"ebitda",
									"ebitda_margin_pct",
									"employee_growth_pct",
									"employees",
									"equity",
									"equity_ratio_pct",
									"id",
									"industry_tags",
									"nace_code",
									"name",
									"org_number",
									"postal_code",
									"profit",
									"profit_margin_pct",
									"region",
									"revenue",
									"revenue_growth_pct",
									"revenue_growth_streak",
									"size_tier",
									"status",
									"website",
									"year_founded"
								]
							},
							"operator": {
								"type": "string",
								"enum": [
									"eq",
									"neq",
									"gt",
									"gte",
									"lt",
									"lte",
									"starts_with",
									"contains",
									"in"
								],
								"description": "How to compare the field against `value`."
							},
							"value": {
								"description": "The value to compare against. Its type follows the field: a number for a financial field, a string for a name or a code, and an array for `in` or for a field that accepts a list."
							}
						},
						"required": [
							"field",
							"operator",
							"value"
						],
						"additionalProperties": false
					},
					{
						"type": "object",
						"properties": {
							"and": {
								"type": "array",
								"items": {
									"$ref": "#/components/schemas/CompanyQuery"
								}
							}
						},
						"required": [
							"and"
						],
						"additionalProperties": false
					},
					{
						"type": "object",
						"properties": {
							"or": {
								"type": "array",
								"items": {
									"$ref": "#/components/schemas/CompanyQuery"
								}
							}
						},
						"required": [
							"or"
						],
						"additionalProperties": false
					},
					{
						"type": "object",
						"properties": {
							"not": {
								"$ref": "#/components/schemas/CompanyQuery"
							}
						},
						"required": [
							"not"
						],
						"additionalProperties": false
					}
				]
			},
			"Error": {
				"type": "object",
				"required": [
					"error",
					"request_id"
				],
				"properties": {
					"error": {
						"type": "object",
						"required": [
							"code",
							"message"
						],
						"description": "What went wrong. Present on every error response, and never alongside `data`.",
						"properties": {
							"code": {
								"type": "string",
								"enum": [
									"missing_api_key",
									"invalid_api_key",
									"invalid_token",
									"origin_not_allowed",
									"invalid_request",
									"invalid_id_prefix",
									"limit_exceeds_maximum",
									"invalid_cursor",
									"query_invalid",
									"query_too_complex",
									"search_timeout",
									"not_found",
									"portfolio_add_limit_reached",
									"monitored_company_limit_reached",
									"rate_limit_exceeded",
									"daily_row_limit_exceeded",
									"research_concurrency_exceeded",
									"company_not_in_portfolio",
									"saved_view_sync_too_large",
									"saved_view_sync_plan_limit_reached",
									"saved_view_size_unavailable",
									"cost_limit_reached",
									"research_unavailable",
									"research_timeout",
									"api_unavailable",
									"filings_unavailable",
									"news_unavailable",
									"similar_companies_unavailable",
									"capability_not_granted",
									"internal_error"
								],
								"description": "The stable machine-readable reason. Branch on this: messages are written for people and may be reworded, but a code only changes when the API version does."
							},
							"message": {
								"type": "string",
								"description": "A human-readable explanation, written for a developer reading a log. Do not match on it."
							},
							"details": {
								"description": "Extra context, when the code has something specific to add. Its shape follows the code: a validation failure carries the offending field paths, a query error carries a JSON path into the tree, and a limit error carries the limit. Absent on codes with nothing to add."
							}
						}
					},
					"request_id": {
						"type": "string",
						"description": "Identifier for this request, also returned in the `X-Request-Id` header. Quote it in support requests: it resolves to the server-side error report."
					}
				}
			},
			"Organization": {
				"type": "object",
				"properties": {
					"id": {
						"type": "string",
						"description": "Your organization’s id, prefixed `org_`. Stable for the lifetime of the organization, and the value to key on when you store which Aira account a credential belongs to."
					},
					"organization_name": {
						"type": "string",
						"description": "Your organization’s display name. Safe to show a user, and not safe to key on: it changes when the organization renames itself."
					}
				},
				"required": [
					"id",
					"organization_name"
				]
			},
			"CompanySummary": {
				"type": "object",
				"properties": {
					"id": {
						"type": "string",
						"description": "Aira’s id for this company, prefixed `cmp_`. Stable, and the id every other endpoint on this API takes."
					},
					"portfolio_entry_id": {
						"nullable": true,
						"description": "Your organization’s portfolio entry for this company, prefixed `pfe_`, or null when you do not hold it. On `GET /v1/companies/{id}` it is also the discriminator between the full record and the summary.",
						"type": "string"
					},
					"org_number": {
						"nullable": true,
						"description": "National registration number, formatted as the registry writes it, separators included. Unique within a country rather than globally.",
						"type": "string"
					},
					"name": {
						"nullable": true,
						"description": "Registered company name.",
						"type": "string"
					},
					"country": {
						"nullable": true,
						"description": "ISO 3166-1 alpha-2 code of the country the company is registered in.",
						"type": "string"
					},
					"city": {
						"nullable": true,
						"description": "Postal town. Coverage is uneven by market, and it is thinner on a search row than on the full company record: GB, IE and DK companies commonly have a city on the full record and null on a search row for the same company.",
						"type": "string"
					},
					"postal_code": {
						"nullable": true,
						"description": "Postal code as the registry records it. Same market unevenness as `city`.",
						"type": "string"
					},
					"aira_url": {
						"description": "Link to this company in Aira. Show it beside the company’s name. The full company page when your organization holds it, the preview page when it does not.",
						"type": "string"
					}
				},
				"required": [
					"id",
					"portfolio_entry_id",
					"org_number",
					"name",
					"country",
					"city",
					"postal_code"
				]
			},
			"searchCompaniesRequest": {
				"type": "object",
				"properties": {
					"query": {
						"allOf": [
							{
								"$ref": "#/components/schemas/CompanyQuery"
							}
						]
					},
					"query_string": {
						"description": "The query written as a string instead of a tree — `country = \"SE\" AND revenue >= 50000000`. Send this or `query`, never both. See the description for the grammar.",
						"type": "string"
					},
					"limit": {
						"description": "Rows per page. Defaults to 50, maximum 99.",
						"maximum": 99,
						"default": 50,
						"type": "integer",
						"minimum": 1
					},
					"cursor": {
						"description": "Echo `meta.next_cursor` from the previous page here, alongside the same `query`. Opaque: never construct one.",
						"type": "string"
					},
					"include_branch_offices": {
						"description": "Set true to include branch offices, which are excluded by default. One control rather than two: a branch office is a non-headquarters foreign company, so lifting either underlying default on its own would change nothing you could observe.",
						"type": "boolean"
					}
				}
			},
			"PortfolioCompany": {
				"type": "object",
				"properties": {
					"id": {
						"nullable": true,
						"description": "Aira’s id for this company, prefixed `cmp_`. **Null when we hold no company-data match for this portfolio entry**, which is the case for roughly a third of portfolios — companies that arrived through email sync or a CRM import carry no match. Use `portfolio_entry_id` as the stable key for a row on this endpoint; it is never null here.",
						"type": "string"
					},
					"portfolio_entry_id": {
						"type": "string",
						"description": "Your organization’s portfolio entry for this company, prefixed `pfe_`. Always present on this endpoint — every row is a portfolio entry — which is what makes it the stable key to use when `id` is null."
					},
					"org_number": {
						"nullable": true,
						"description": "National registration number, formatted as the registry writes it, separators included. Unique within a country rather than globally.",
						"type": "string"
					},
					"name": {
						"nullable": true,
						"description": "Registered company name.",
						"type": "string"
					},
					"country": {
						"nullable": true,
						"description": "ISO 3166-1 alpha-2 code of the country the company is registered in.",
						"type": "string"
					},
					"city": {
						"nullable": true,
						"description": "Postal town. Coverage is uneven by market, and it is thinner on a search row than on the full company record: GB, IE and DK companies commonly have a city on the full record and null on a search row for the same company.",
						"type": "string"
					},
					"postal_code": {
						"nullable": true,
						"description": "Postal code as the registry records it. Same market unevenness as `city`.",
						"type": "string"
					},
					"aira_url": {
						"description": "Link to this company in Aira. Show it beside the company’s name. The full company page when your organization holds it, the preview page when it does not.",
						"type": "string"
					},
					"status": {
						"nullable": true,
						"description": "Registry status. `active` and `inactive` cover effectively the whole corpus; the remainder are `bankruptcy`, `bankruptcy_completed`, `bankruptcy_ongoing`, `fusion_ongoing`, `liquidated`, `liquidation_ongoing`, `merged`, `reconstruction_ongoing`, `shell_corporation`, `other` and `unknown`. Not published as a closed enum: the value is passed through from the registry feed, so treat an unfamiliar one as data rather than as an error.",
						"type": "string"
					},
					"company_form": {
						"nullable": true,
						"description": "Legal form as a code normalized across markets, from the same set the `company_form` search filter takes.",
						"type": "string"
					},
					"registration_date": {
						"nullable": true,
						"type": "string",
						"description": "When the company was first registered.",
						"format": "date"
					},
					"vat_number": {
						"nullable": true,
						"description": "VAT registration number, where we hold one.",
						"type": "string"
					},
					"nace_code": {
						"nullable": true,
						"description": "NACE Rev. 2 industry code, the EU standard, formatted for display (`68.200`). ⚠️ Do not feed this value back into the `nace_code` filter: the searchable corpus stores these WITHOUT a separator and at a different depth, so filtering on the value shown here matches almost nothing. Filter on a 2-character division prefix instead — see that field’s own description.",
						"type": "string"
					},
					"sni_code": {
						"nullable": true,
						"description": "National industry code, where the market publishes one alongside NACE. Null in most markets.",
						"type": "string"
					},
					"industry_tags": {
						"type": "array",
						"items": {
							"type": "string"
						},
						"description": "Aira’s own industry tags. Sparsely populated, so an empty array is the normal answer and their absence says nothing about the company."
					},
					"size_tier": {
						"nullable": true,
						"description": "EU SME size tier (2003/361/EC): `micro`, `small`, `medium` or `large`. Null for the majority of companies, which we cannot tier.",
						"type": "string"
					},
					"website": {
						"nullable": true,
						"description": "Primary website, as a bare host without a scheme, for example `volvo.com`.",
						"type": "string"
					},
					"domains": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"domain": {
									"type": "string",
									"description": "A domain associated with this company, as a bare host."
								},
								"source": {
									"type": "string",
									"enum": [
										"company_record",
										"email_domain",
										"web_match",
										"manual",
										"web_search",
										"other"
									],
									"description": "How we came to associate this domain with the company, which is how much to trust it. `company_record` is stated on the company’s data record; `email_domain` is derived from an email address on that record; `web_match` was matched by our crawler; `manual` was set by a person at Aira; `web_search` came from an automated lookup."
								},
								"primary": {
									"type": "boolean",
									"description": "True for the domain we treat as the company’s main web presence."
								}
							},
							"required": [
								"domain",
								"source",
								"primary"
							]
						},
						"description": "Every domain we associate with the company, not just the primary one. An empty array is common."
					},
					"phone": {
						"nullable": true,
						"description": "Primary telephone number as the source records it. Local formatting is preserved and it is not normalized to E.164, so parse before you dial it.",
						"type": "string"
					},
					"employees": {
						"nullable": true,
						"description": "Headcount, exact. Null — never zero — where the source publishes a range instead; `employee_band` then carries it and is the only headcount we hold. The two are mutually exclusive, and both are null where we hold no headcount.",
						"type": "number"
					},
					"employee_band": {
						"nullable": true,
						"description": "The headcount range the source publishes instead of a figure, in the markets that do (DK, and most of DE). Set exactly when `employees` is null for that reason; null means the headcount is exact — read `employees` — or absent.",
						"type": "object",
						"properties": {
							"min": {
								"nullable": true,
								"description": "Bottom of the reported band.",
								"type": "number"
							},
							"max": {
								"nullable": true,
								"description": "Top of the reported band, or null on an open-ended top band.",
								"type": "number"
							}
						},
						"required": [
							"min",
							"max"
						]
					},
					"revenue": {
						"nullable": true,
						"description": "Revenue from the most recent filed statement, in `currency`, full units. Null when we hold no filed figure, in which case `estimated_revenue` may still carry a modelled range.",
						"type": "number"
					},
					"estimated_revenue": {
						"nullable": true,
						"description": "A modelled revenue range, present only when there is no filed figure. Never both: `revenue` and this field are the filed and the modelled answer to the same question.",
						"type": "object",
						"properties": {
							"low": {
								"type": "number",
								"description": "Bottom of the modelled range, in `currency`, full units."
							},
							"high": {
								"type": "number",
								"description": "Top of the modelled range, in `currency`, full units."
							},
							"median_error_pct": {
								"type": "number",
								"description": "The model’s median error for companies like this one, as a percentage. A guide to how wide the range really is, not a confidence interval."
							}
						},
						"required": [
							"low",
							"high",
							"median_error_pct"
						]
					},
					"currency": {
						"nullable": true,
						"description": "ISO 4217 code for every top-level monetary field on this record. Null when we hold no financials. It is the company’s current reporting currency, so figures are not comparable across companies without converting them. Each entry in `financials[]` carries its own `currency`, which matches this one unless the company changed reporting currency.",
						"type": "string"
					},
					"ebitda": {
						"nullable": true,
						"description": "EBITDA from the most recent filed statement, in `currency`, full units.",
						"type": "number"
					},
					"profit": {
						"nullable": true,
						"description": "Net profit from the most recent filed statement, in `currency`, full units. This is what the `profit` search filter targets.",
						"type": "number"
					},
					"total_equity": {
						"nullable": true,
						"description": "Total equity from the most recent filed statement, in `currency`, full units. The search filter for this figure is called `equity`.",
						"type": "number"
					},
					"cash_and_bank": {
						"nullable": true,
						"description": "Cash and bank balances from the most recent filed statement, in `currency`, full units.",
						"type": "number"
					},
					"profit_margin_pct": {
						"nullable": true,
						"description": "Profit margin as a percentage, from the most recent filed statement.",
						"type": "number"
					},
					"revenue_growth_pct": {
						"nullable": true,
						"description": "Year-over-year revenue change as a percentage, between the two latest periods.",
						"type": "number"
					},
					"financials": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"period_end": {
									"type": "string",
									"description": "Closing date of the reporting period.",
									"format": "date"
								},
								"currency": {
									"description": "ISO 4217 code for every monetary field in this period. The period’s own filed currency, so it can differ from the record-level `currency` for a company that changed reporting currency. Null when the filing does not state one.",
									"nullable": true,
									"type": "string"
								},
								"revenue": {
									"nullable": true,
									"description": "Total operating income for the period, in `currency`, full units. In markets whose filings state only net turnover, the filed turnover figure is what we hold.",
									"type": "number"
								},
								"operating_profit": {
									"nullable": true,
									"description": "Operating result for the period, in `currency`, full units.",
									"type": "number"
								},
								"profit_before_tax": {
									"nullable": true,
									"description": "Profit after financial items and before tax, in `currency`, full units.",
									"type": "number"
								},
								"net_profit": {
									"nullable": true,
									"description": "Profit for the period after tax, in `currency`, full units.",
									"type": "number"
								},
								"ebitda": {
									"nullable": true,
									"description": "EBITDA for the period, in `currency`, full units.",
									"type": "number"
								},
								"total_equity": {
									"nullable": true,
									"description": "Total equity at the period end, in `currency`, full units.",
									"type": "number"
								},
								"employees": {
									"nullable": true,
									"description": "Average headcount reported for the period, exact. Null where the filing carries none, and on a company the source publishes as a range — there is no per-period band, so read the company-level `employee_band`, which does not vary by year.",
									"type": "number"
								}
							},
							"required": [
								"period_end",
								"revenue",
								"operating_profit",
								"profit_before_tax",
								"net_profit",
								"ebitda",
								"total_equity",
								"employees"
							]
						},
						"description": "Filed annual statements, newest first. A statement whose period end is unknown is not returned, because it cannot be placed on a timeline. An empty array means we hold no filed statements."
					},
					"description": {
						"nullable": true,
						"description": "A short description of what the company does, in English, written by Aira from the company’s own website. Null when we have not scraped it.",
						"type": "string"
					},
					"description_long": {
						"nullable": true,
						"description": "A longer form of `description`, from the same source and null in the same cases.",
						"type": "string"
					},
					"last_scraped_at": {
						"nullable": true,
						"type": "string",
						"description": "When we last read the company website that `description` is written from. It says nothing about the freshness of the registry data on the rest of this record.",
						"format": "date-time"
					},
					"activity_text": {
						"nullable": true,
						"description": "The company’s own business-activity statement as filed with the registry, in the registry’s language. Populated for Swedish companies and rare elsewhere.",
						"type": "string"
					},
					"addresses": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"type": {
									"type": "string",
									"enum": [
										"post",
										"registered",
										"visiting"
									],
									"description": "Which address this is: `post` for the postal address, `registered` for the registered office, `visiting` for the visiting address."
								},
								"street": {
									"nullable": true,
									"description": "Street address, including any number.",
									"type": "string"
								},
								"city": {
									"nullable": true,
									"description": "Postal town for this address block.",
									"type": "string"
								},
								"postal_code": {
									"nullable": true,
									"description": "Postal code for this address block.",
									"type": "string"
								},
								"country": {
									"nullable": true,
									"description": "ISO 3166-1 alpha-2 code for this address block’s country.",
									"type": "string"
								},
								"latitude": {
									"nullable": true,
									"description": "WGS 84 latitude, where we hold coordinates for this block.",
									"type": "number"
								},
								"longitude": {
									"nullable": true,
									"description": "WGS 84 longitude, where we hold coordinates for this block.",
									"type": "number"
								}
							},
							"required": [
								"type",
								"street",
								"city",
								"postal_code",
								"country",
								"latitude",
								"longitude"
							]
						},
						"description": "Every address block we hold, in resolution precedence order. `city` and `postal_code` on this record are taken from the first block that carries them, so use this array when you need a full mailable address or need to know which kind you have."
					},
					"directors": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"name": {
									"type": "string",
									"description": "The person’s name as the registry records it."
								},
								"role": {
									"nullable": true,
									"description": "The role as the registry names it, in the registry’s own vocabulary and language. Not normalized across markets.",
									"type": "string"
								},
								"role_start_date": {
									"nullable": true,
									"type": "string",
									"description": "When the person took the role, where the registry records it.",
									"format": "date"
								}
							},
							"required": [
								"name",
								"role",
								"role_start_date"
							]
						},
						"description": "People currently registered as holding a role at the company. Anyone whose role has ended is not returned, so this is a present-day board and not a history."
					},
					"owner": {
						"description": "The person this company is assigned to in your organization, or null when nobody is assigned — which is the case for most companies. Users are mirrored between Aira and a connected CRM, so this is also the company’s assigned user in that CRM.",
						"nullable": true,
						"type": "object",
						"properties": {
							"name": {
								"nullable": true,
								"description": "The assigned user’s name.",
								"type": "string"
							},
							"email": {
								"type": "string",
								"description": "The assigned user’s email address."
							}
						},
						"required": [
							"name",
							"email"
						]
					}
				},
				"required": [
					"id",
					"portfolio_entry_id",
					"org_number",
					"name",
					"country",
					"city",
					"postal_code",
					"status",
					"company_form",
					"registration_date",
					"vat_number",
					"nace_code",
					"sni_code",
					"industry_tags",
					"size_tier",
					"website",
					"domains",
					"phone",
					"employees",
					"employee_band",
					"revenue",
					"estimated_revenue",
					"currency",
					"ebitda",
					"profit",
					"total_equity",
					"cash_and_bank",
					"profit_margin_pct",
					"revenue_growth_pct",
					"financials",
					"description",
					"description_long",
					"last_scraped_at",
					"activity_text",
					"addresses",
					"directors"
				]
			},
			"Company": {
				"type": "object",
				"properties": {
					"id": {
						"type": "string",
						"description": "Aira’s id for this company, prefixed `cmp_`. Stable, and the id every other endpoint on this API takes."
					},
					"portfolio_entry_id": {
						"nullable": true,
						"description": "Your organization’s portfolio entry for this company, prefixed `pfe_`, or null when you do not hold it. On `GET /v1/companies/{id}` it is also the discriminator between the full record and the summary.",
						"type": "string"
					},
					"org_number": {
						"nullable": true,
						"description": "National registration number, formatted as the registry writes it, separators included. Unique within a country rather than globally.",
						"type": "string"
					},
					"name": {
						"nullable": true,
						"description": "Registered company name.",
						"type": "string"
					},
					"country": {
						"nullable": true,
						"description": "ISO 3166-1 alpha-2 code of the country the company is registered in.",
						"type": "string"
					},
					"city": {
						"nullable": true,
						"description": "Postal town. Coverage is uneven by market, and it is thinner on a search row than on the full company record: GB, IE and DK companies commonly have a city on the full record and null on a search row for the same company.",
						"type": "string"
					},
					"postal_code": {
						"nullable": true,
						"description": "Postal code as the registry records it. Same market unevenness as `city`.",
						"type": "string"
					},
					"aira_url": {
						"description": "Link to this company in Aira. Show it beside the company’s name. The full company page when your organization holds it, the preview page when it does not.",
						"type": "string"
					},
					"status": {
						"nullable": true,
						"description": "Registry status. `active` and `inactive` cover effectively the whole corpus; the remainder are `bankruptcy`, `bankruptcy_completed`, `bankruptcy_ongoing`, `fusion_ongoing`, `liquidated`, `liquidation_ongoing`, `merged`, `reconstruction_ongoing`, `shell_corporation`, `other` and `unknown`. Not published as a closed enum: the value is passed through from the registry feed, so treat an unfamiliar one as data rather than as an error.",
						"type": "string"
					},
					"company_form": {
						"nullable": true,
						"description": "Legal form as a code normalized across markets, from the same set the `company_form` search filter takes.",
						"type": "string"
					},
					"registration_date": {
						"nullable": true,
						"type": "string",
						"description": "When the company was first registered.",
						"format": "date"
					},
					"vat_number": {
						"nullable": true,
						"description": "VAT registration number, where we hold one.",
						"type": "string"
					},
					"nace_code": {
						"nullable": true,
						"description": "NACE Rev. 2 industry code, the EU standard, formatted for display (`68.200`). ⚠️ Do not feed this value back into the `nace_code` filter: the searchable corpus stores these WITHOUT a separator and at a different depth, so filtering on the value shown here matches almost nothing. Filter on a 2-character division prefix instead — see that field’s own description.",
						"type": "string"
					},
					"sni_code": {
						"nullable": true,
						"description": "National industry code, where the market publishes one alongside NACE. Null in most markets.",
						"type": "string"
					},
					"industry_tags": {
						"type": "array",
						"items": {
							"type": "string"
						},
						"description": "Aira’s own industry tags. Sparsely populated, so an empty array is the normal answer and their absence says nothing about the company."
					},
					"size_tier": {
						"nullable": true,
						"description": "EU SME size tier (2003/361/EC): `micro`, `small`, `medium` or `large`. Null for the majority of companies, which we cannot tier.",
						"type": "string"
					},
					"website": {
						"nullable": true,
						"description": "Primary website, as a bare host without a scheme, for example `volvo.com`.",
						"type": "string"
					},
					"domains": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"domain": {
									"type": "string",
									"description": "A domain associated with this company, as a bare host."
								},
								"source": {
									"type": "string",
									"enum": [
										"company_record",
										"email_domain",
										"web_match",
										"manual",
										"web_search",
										"other"
									],
									"description": "How we came to associate this domain with the company, which is how much to trust it. `company_record` is stated on the company’s data record; `email_domain` is derived from an email address on that record; `web_match` was matched by our crawler; `manual` was set by a person at Aira; `web_search` came from an automated lookup."
								},
								"primary": {
									"type": "boolean",
									"description": "True for the domain we treat as the company’s main web presence."
								}
							},
							"required": [
								"domain",
								"source",
								"primary"
							]
						},
						"description": "Every domain we associate with the company, not just the primary one. An empty array is common."
					},
					"phone": {
						"nullable": true,
						"description": "Primary telephone number as the source records it. Local formatting is preserved and it is not normalized to E.164, so parse before you dial it.",
						"type": "string"
					},
					"employees": {
						"nullable": true,
						"description": "Headcount, exact. Null — never zero — where the source publishes a range instead; `employee_band` then carries it and is the only headcount we hold. The two are mutually exclusive, and both are null where we hold no headcount.",
						"type": "number"
					},
					"employee_band": {
						"nullable": true,
						"description": "The headcount range the source publishes instead of a figure, in the markets that do (DK, and most of DE). Set exactly when `employees` is null for that reason; null means the headcount is exact — read `employees` — or absent.",
						"type": "object",
						"properties": {
							"min": {
								"nullable": true,
								"description": "Bottom of the reported band.",
								"type": "number"
							},
							"max": {
								"nullable": true,
								"description": "Top of the reported band, or null on an open-ended top band.",
								"type": "number"
							}
						},
						"required": [
							"min",
							"max"
						]
					},
					"revenue": {
						"nullable": true,
						"description": "Revenue from the most recent filed statement, in `currency`, full units. Null when we hold no filed figure, in which case `estimated_revenue` may still carry a modelled range.",
						"type": "number"
					},
					"estimated_revenue": {
						"nullable": true,
						"description": "A modelled revenue range, present only when there is no filed figure. Never both: `revenue` and this field are the filed and the modelled answer to the same question.",
						"type": "object",
						"properties": {
							"low": {
								"type": "number",
								"description": "Bottom of the modelled range, in `currency`, full units."
							},
							"high": {
								"type": "number",
								"description": "Top of the modelled range, in `currency`, full units."
							},
							"median_error_pct": {
								"type": "number",
								"description": "The model’s median error for companies like this one, as a percentage. A guide to how wide the range really is, not a confidence interval."
							}
						},
						"required": [
							"low",
							"high",
							"median_error_pct"
						]
					},
					"currency": {
						"nullable": true,
						"description": "ISO 4217 code for every top-level monetary field on this record. Null when we hold no financials. It is the company’s current reporting currency, so figures are not comparable across companies without converting them. Each entry in `financials[]` carries its own `currency`, which matches this one unless the company changed reporting currency.",
						"type": "string"
					},
					"ebitda": {
						"nullable": true,
						"description": "EBITDA from the most recent filed statement, in `currency`, full units.",
						"type": "number"
					},
					"profit": {
						"nullable": true,
						"description": "Net profit from the most recent filed statement, in `currency`, full units. This is what the `profit` search filter targets.",
						"type": "number"
					},
					"total_equity": {
						"nullable": true,
						"description": "Total equity from the most recent filed statement, in `currency`, full units. The search filter for this figure is called `equity`.",
						"type": "number"
					},
					"cash_and_bank": {
						"nullable": true,
						"description": "Cash and bank balances from the most recent filed statement, in `currency`, full units.",
						"type": "number"
					},
					"profit_margin_pct": {
						"nullable": true,
						"description": "Profit margin as a percentage, from the most recent filed statement.",
						"type": "number"
					},
					"revenue_growth_pct": {
						"nullable": true,
						"description": "Year-over-year revenue change as a percentage, between the two latest periods.",
						"type": "number"
					},
					"financials": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"period_end": {
									"type": "string",
									"description": "Closing date of the reporting period.",
									"format": "date"
								},
								"currency": {
									"description": "ISO 4217 code for every monetary field in this period. The period’s own filed currency, so it can differ from the record-level `currency` for a company that changed reporting currency. Null when the filing does not state one.",
									"nullable": true,
									"type": "string"
								},
								"revenue": {
									"nullable": true,
									"description": "Total operating income for the period, in `currency`, full units. In markets whose filings state only net turnover, the filed turnover figure is what we hold.",
									"type": "number"
								},
								"operating_profit": {
									"nullable": true,
									"description": "Operating result for the period, in `currency`, full units.",
									"type": "number"
								},
								"profit_before_tax": {
									"nullable": true,
									"description": "Profit after financial items and before tax, in `currency`, full units.",
									"type": "number"
								},
								"net_profit": {
									"nullable": true,
									"description": "Profit for the period after tax, in `currency`, full units.",
									"type": "number"
								},
								"ebitda": {
									"nullable": true,
									"description": "EBITDA for the period, in `currency`, full units.",
									"type": "number"
								},
								"total_equity": {
									"nullable": true,
									"description": "Total equity at the period end, in `currency`, full units.",
									"type": "number"
								},
								"employees": {
									"nullable": true,
									"description": "Average headcount reported for the period, exact. Null where the filing carries none, and on a company the source publishes as a range — there is no per-period band, so read the company-level `employee_band`, which does not vary by year.",
									"type": "number"
								}
							},
							"required": [
								"period_end",
								"revenue",
								"operating_profit",
								"profit_before_tax",
								"net_profit",
								"ebitda",
								"total_equity",
								"employees"
							]
						},
						"description": "Filed annual statements, newest first. A statement whose period end is unknown is not returned, because it cannot be placed on a timeline. An empty array means we hold no filed statements."
					},
					"description": {
						"nullable": true,
						"description": "A short description of what the company does, in English, written by Aira from the company’s own website. Null when we have not scraped it.",
						"type": "string"
					},
					"description_long": {
						"nullable": true,
						"description": "A longer form of `description`, from the same source and null in the same cases.",
						"type": "string"
					},
					"last_scraped_at": {
						"nullable": true,
						"type": "string",
						"description": "When we last read the company website that `description` is written from. It says nothing about the freshness of the registry data on the rest of this record.",
						"format": "date-time"
					},
					"activity_text": {
						"nullable": true,
						"description": "The company’s own business-activity statement as filed with the registry, in the registry’s language. Populated for Swedish companies and rare elsewhere.",
						"type": "string"
					},
					"addresses": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"type": {
									"type": "string",
									"enum": [
										"post",
										"registered",
										"visiting"
									],
									"description": "Which address this is: `post` for the postal address, `registered` for the registered office, `visiting` for the visiting address."
								},
								"street": {
									"nullable": true,
									"description": "Street address, including any number.",
									"type": "string"
								},
								"city": {
									"nullable": true,
									"description": "Postal town for this address block.",
									"type": "string"
								},
								"postal_code": {
									"nullable": true,
									"description": "Postal code for this address block.",
									"type": "string"
								},
								"country": {
									"nullable": true,
									"description": "ISO 3166-1 alpha-2 code for this address block’s country.",
									"type": "string"
								},
								"latitude": {
									"nullable": true,
									"description": "WGS 84 latitude, where we hold coordinates for this block.",
									"type": "number"
								},
								"longitude": {
									"nullable": true,
									"description": "WGS 84 longitude, where we hold coordinates for this block.",
									"type": "number"
								}
							},
							"required": [
								"type",
								"street",
								"city",
								"postal_code",
								"country",
								"latitude",
								"longitude"
							]
						},
						"description": "Every address block we hold, in resolution precedence order. `city` and `postal_code` on this record are taken from the first block that carries them, so use this array when you need a full mailable address or need to know which kind you have."
					},
					"directors": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"name": {
									"type": "string",
									"description": "The person’s name as the registry records it."
								},
								"role": {
									"nullable": true,
									"description": "The role as the registry names it, in the registry’s own vocabulary and language. Not normalized across markets.",
									"type": "string"
								},
								"role_start_date": {
									"nullable": true,
									"type": "string",
									"description": "When the person took the role, where the registry records it.",
									"format": "date"
								}
							},
							"required": [
								"name",
								"role",
								"role_start_date"
							]
						},
						"description": "People currently registered as holding a role at the company. Anyone whose role has ended is not returned, so this is a present-day board and not a history."
					},
					"owner": {
						"description": "The person this company is assigned to in your organization, or null when nobody is assigned — which is the case for most companies. Users are mirrored between Aira and a connected CRM, so this is also the company’s assigned user in that CRM.",
						"nullable": true,
						"type": "object",
						"properties": {
							"name": {
								"nullable": true,
								"description": "The assigned user’s name.",
								"type": "string"
							},
							"email": {
								"type": "string",
								"description": "The assigned user’s email address."
							}
						},
						"required": [
							"name",
							"email"
						]
					}
				},
				"required": [
					"id",
					"portfolio_entry_id",
					"org_number",
					"name",
					"country",
					"city",
					"postal_code",
					"status",
					"company_form",
					"registration_date",
					"vat_number",
					"nace_code",
					"sni_code",
					"industry_tags",
					"size_tier",
					"website",
					"domains",
					"phone",
					"employees",
					"employee_band",
					"revenue",
					"estimated_revenue",
					"currency",
					"ebitda",
					"profit",
					"total_equity",
					"cash_and_bank",
					"profit_margin_pct",
					"revenue_growth_pct",
					"financials",
					"description",
					"description_long",
					"last_scraped_at",
					"activity_text",
					"addresses",
					"directors"
				]
			},
			"Contact": {
				"type": "object",
				"properties": {
					"name": {
						"type": "string",
						"description": "The person’s name as the source records it. Usually a full name; for some people only a first name and last initial, such as `Johan R.`, whose `email_status` is then `locked` or `not_found`. Once someone in your organization reveals that person’s email, their full name appears here instead."
					},
					"title": {
						"nullable": true,
						"description": "Job title in the source’s own words and language. Not normalized to a seniority or a function, so match on it loosely.",
						"type": "string"
					},
					"email": {
						"nullable": true,
						"description": "Work email address. Null when we hold none, and also null when the one we hold may not be passed on to you, so a null does not mean the person has no address. `email_status` says which.",
						"type": "string"
					},
					"email_status": {
						"description": "Why `email` is or is not set. `available`: the address is in `email`. `locked`: withheld until someone in your organization reveals it in Aira, after which the address appears here. `not_found`: there is no address we can give you here.",
						"type": "string",
						"enum": [
							"available",
							"locked",
							"not_found"
						]
					},
					"phone": {
						"nullable": true,
						"description": "Telephone number as the source records it. Local formatting is preserved and it is not normalized to E.164.",
						"type": "string"
					},
					"linkedin_url": {
						"nullable": true,
						"description": "A LinkedIn profile URL for this person, best effort, and null more often than not. It is a link and nothing more: no LinkedIn profile data sits behind it, and we do not verify that it still resolves.",
						"type": "string"
					}
				},
				"required": [
					"name",
					"title",
					"email",
					"phone",
					"linkedin_url"
				]
			},
			"Filing": {
				"type": "object",
				"properties": {
					"id": {
						"type": "string",
						"description": "Stable identifier for this filing."
					},
					"company": {
						"type": "object",
						"properties": {
							"id": {
								"type": "string",
								"description": "The company this filing is about."
							},
							"portfolio_entry_id": {
								"type": "string",
								"description": "Your portfolio entry for this company. Always present on a filing."
							},
							"name": {
								"nullable": true,
								"description": "The company name as the registry recorded it.",
								"type": "string"
							},
							"aira_url": {
								"description": "Link to this company in Aira, for a person to open.",
								"type": "string"
							}
						},
						"required": [
							"id",
							"portfolio_entry_id",
							"name"
						],
						"description": "The company the filing is about. Three fields rather than the full record: a filings page can carry 500 rows, and embedding a metered company record on each of them would hand the product away."
					},
					"filed_on": {
						"type": "string",
						"description": "The date the registry recorded the change. Date-only: the source carries no time component.",
						"format": "date"
					},
					"type": {
						"type": "string",
						"enum": [
							"address_changed",
							"annual_account_changed",
							"annual_account_new",
							"annual_report_new",
							"auditor_not_required_by_articles",
							"auditor_required_by_articles",
							"authorised_signatories_changed",
							"board_changed",
							"business_activity_changed",
							"company_mortgage_changed",
							"company_mortgage_new",
							"company_mortgage_removed",
							"company_name_changed",
							"company_representatives_changed",
							"company_tax_changed",
							"company_unit_name_changed",
							"company_unit_new",
							"employment_tax_changed",
							"fax_changed",
							"fax_removed",
							"group_annual_account_changed",
							"group_annual_account_new",
							"group_structure_changed",
							"industry_code_changed",
							"iou_changed",
							"iou_exchange_information_changed",
							"merger_information_changed",
							"new_unit",
							"ongoing_matter_new",
							"other",
							"phone_changed",
							"phone_new",
							"phone_removed",
							"place_of_residence_changed",
							"registration_date_changed",
							"secondary_industry_changed",
							"share_capital_bonus_issue",
							"share_capital_changed",
							"share_capital_new",
							"share_capital_new_issue",
							"share_capital_reduction",
							"share_class_changed",
							"share_class_new",
							"status_changed",
							"status_new",
							"vat_changed"
						],
						"description": "What changed, as a stable published value. Never a raw source code."
					},
					"description": {
						"nullable": true,
						"description": "The registry’s own English description of the change.",
						"type": "string"
					}
				},
				"required": [
					"id",
					"company",
					"filed_on",
					"type",
					"description"
				]
			},
			"NewsItem": {
				"type": "object",
				"properties": {
					"id": {
						"type": "string",
						"description": "Aira’s id for this article, prefixed `nws_`. It identifies the representative article of the story, not the story."
					},
					"story_id": {
						"nullable": true,
						"description": "Aira’s id for the event the article covers, prefixed `sty_`, shared by every article covering it including later re-covers. This is what you join on to avoid processing the same news twice. Null for a minority of items that were never grouped: fall back to `id`, but do not treat it as a story key, because a second article about the same event will not share it.",
						"type": "string"
					},
					"title": {
						"type": "string",
						"description": "The article headline, as published."
					},
					"url": {
						"type": "string",
						"description": "The one canonical link to the article on the publisher’s site. There is no second spelling of it on this resource."
					},
					"published_at": {
						"type": "string",
						"description": "When the publisher dated the article. A small tail of articles sits exactly at midnight, where the time of day was never known upstream, so do not read `00:00:00` as a real publication time.",
						"format": "date-time"
					},
					"language": {
						"nullable": true,
						"description": "ISO 639-1 code for the language the article is written in, or null when it could not be determined.",
						"type": "string"
					},
					"source": {
						"type": "string",
						"enum": [
							"media",
							"company_website"
						],
						"description": "`media` for third-party press coverage, `company_website` for something the company published about itself."
					},
					"companies": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"company_id": {
									"type": "string",
									"description": "The company this article is about, prefixed `cmp_`."
								},
								"portfolio_entry_id": {
									"type": "string",
									"description": "Your organization’s portfolio entry for that company, prefixed `pfe_`. Always present: an item that matched no entry of yours is not returned."
								},
								"aira_url": {
									"description": "Link to that company in Aira, for a person to open.",
									"type": "string"
								}
							},
							"required": [
								"company_id",
								"portfolio_entry_id"
							]
						},
						"description": "The companies in **your** portfolio that this article is about. Never empty, and never the article’s full company list: an article names about thirteen companies on average, and only the ones you hold appear here."
					},
					"events": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"type": {
									"type": "string",
									"description": "The business event. One of: `acquired_by_other_company`, `acquired_other_company`, `capital_raised`, `delisting`, `divestiture`, `exit_from_market`, `expansion_into_new_market`, `financial_distress`, `force_majeure`, `in_bankruptcy`, `in_liquidation`, `in_reconstruction`, `ipo`, `large_contract_lost`, `large_contract_won`, `layoffs`, `legal_challenge`, `management_change`, `merged_with_other_company`, `partnership`, `product_discontinuation`, `product_launch`, `risk_of_bankruptcy`, `risk_of_reconstruction`, `seeking_acquisition`, `site_closure`, `site_opening`. **Published as a documented list rather than a closed enum**, because the taxonomy is ours to extend and a new type can appear before this document names it. Build your rules on the values you care about and ignore the rest rather than failing on an unfamiliar one."
								},
								"event_date": {
									"nullable": true,
									"type": "string",
									"description": "When the event occurred, which is not the same as when the article was published: an article can report an event weeks after the fact. Null when the article does not date it.",
									"format": "date"
								},
								"confidence": {
									"nullable": true,
									"description": "How sure the classifier is that the article really describes this event. `high` on the large majority of events and `medium` on the rest; a `medium` is worth reviewing before it triggers an outbound action. Documented rather than enumerated for the same reason as `type`.",
									"type": "string"
								},
								"summary": {
									"nullable": true,
									"description": "A one-line statement of what happened, written from the article.",
									"type": "string"
								},
								"reason": {
									"nullable": true,
									"description": "Why the article was classified as this event.",
									"type": "string"
								},
								"details": {
									"nullable": true,
									"description": "Structured detail specific to this event type, for example an amount on a `capital_raised` or a named counterparty on an acquisition. Populated on almost every event. **Null and `{}` both mean there is nothing here to read** — null when the event carried no detail object at all, `{}` when it carried one that held nothing publishable — so handle them alike, and branch on whether the key you want is present rather than on the object being empty. **The keys are freeform:** they vary by event type and new ones appear without a schema change, so read defensively rather than destructuring. Values are always scalars — a string, a number, a boolean or null — and never nest, so nothing here needs recursive handling. Values longer than 1,000 characters, and keys that are not identifiers, are omitted rather than published.",
									"type": "object",
									"additionalProperties": {
										"anyOf": [
											{
												"nullable": true,
												"type": "string"
											},
											{
												"nullable": true,
												"type": "number"
											},
											{
												"nullable": true,
												"type": "boolean"
											}
										]
									}
								}
							},
							"required": [
								"type",
								"event_date",
								"confidence",
								"summary",
								"reason",
								"details"
							]
						},
						"description": "The business events this article describes, and the part of this resource worth automating on. Usually one; an article covering a merger and a management change carries two. An empty array means the article matched your portfolio but described no event we classify."
					}
				},
				"required": [
					"id",
					"story_id",
					"title",
					"url",
					"published_at",
					"language",
					"source",
					"companies",
					"events"
				]
			},
			"PortfolioAddResult": {
				"type": "object",
				"properties": {
					"company_id": {
						"type": "string",
						"description": "The `cmp_` id you requested, echoed back on every outcome. **Match your request to this rather than to array position:** duplicate ids collapse to one entry, so the response can be shorter than the request."
					},
					"outcome": {
						"type": "string",
						"enum": [
							"added",
							"already_in_portfolio",
							"blocked",
							"failed"
						],
						"description": "What happened to this company. `added`: it is now in your portfolio, and it was charged. `already_in_portfolio`: it was there before this call, and it was not charged. `blocked`: an allowance or a limit refused it, and retrying changes nothing until the limit does. `failed`: the id is unknown or something on our side went wrong, and it is worth retrying."
					},
					"portfolio_entry_id": {
						"nullable": true,
						"description": "Your portfolio entry for this company, prefixed `pfe_`. Present on `added` and `already_in_portfolio`, null on `blocked` and `failed`.",
						"type": "string"
					},
					"company": {
						"nullable": true,
						"description": "The full company record, the same shape and the same serializer `GET /v1/companies/{id}` uses for a company you hold. Null on `blocked` and `failed`, and null in the narrow case where the add succeeded but reading the record back did not, in which case the company **is** in your portfolio and a detail call returns it.",
						"allOf": [
							{
								"$ref": "#/components/schemas/Company"
							}
						]
					},
					"error": {
						"nullable": true,
						"description": "Why this company was not added. Null on `added` and `already_in_portfolio`.",
						"type": "object",
						"properties": {
							"code": {
								"type": "string",
								"enum": [
									"not_found",
									"portfolio_add_limit_reached",
									"monitored_company_limit_reached",
									"internal_error"
								],
								"description": "Why this company was not added. The same codes the error reference documents, narrowed to what a single item can carry. Branch on this rather than on `message`."
							},
							"message": {
								"type": "string",
								"description": "A human-readable explanation of this item’s failure. Written for people and may be reworded, so do not match on it."
							},
							"details": {
								"nullable": true,
								"description": "Present on `portfolio_add_limit_reached`, carrying the same `used` and `limit` as `meta.quotas.portfolio_companies`. Null for every other code.",
								"type": "object",
								"properties": {
									"quotas": {
										"type": "object",
										"properties": {
											"portfolio_companies": {
												"type": "object",
												"properties": {
													"used": {
														"type": "number",
														"description": "Companies you currently hold, capped at `limit`."
													},
													"limit": {
														"nullable": true,
														"description": "How many companies you may hold, or null if your plan is unmetered.",
														"type": "number"
													}
												},
												"required": [
													"used",
													"limit"
												],
												"description": "Where you stand on your portfolio allowance."
											}
										},
										"required": [
											"portfolio_companies"
										],
										"description": "The allowance this item ran into, in the shape `GET /v1/quotas` publishes."
									}
								},
								"required": [
									"quotas"
								]
							}
						},
						"required": [
							"code",
							"message",
							"details"
						]
					}
				},
				"required": [
					"company_id",
					"outcome",
					"portfolio_entry_id",
					"company",
					"error"
				]
			},
			"addPortfolioCompaniesRequest": {
				"type": "object",
				"properties": {
					"company_ids": {
						"minItems": 1,
						"maxItems": 100,
						"type": "array",
						"items": {
							"type": "string"
						},
						"description": "Company ids (`cmp_…`) to add to your portfolio, 1 to 100 per request."
					}
				},
				"required": [
					"company_ids"
				]
			},
			"SimilarCompany": {
				"type": "object",
				"properties": {
					"company": {
						"type": "object",
						"properties": {
							"id": {
								"type": "string",
								"description": "The suggested company, prefixed `cmp_`. Pass it to `POST /v1/portfolio/companies` to act on the suggestion, or to `GET /v1/companies/{id}` for its summary."
							},
							"org_number": {
								"nullable": true,
								"description": "National registration number, as the registry writes it.",
								"type": "string"
							},
							"name": {
								"nullable": true,
								"description": "Registered company name.",
								"type": "string"
							},
							"country": {
								"nullable": true,
								"description": "ISO 3166-1 alpha-2 code of the registering country.",
								"type": "string"
							},
							"city": {
								"nullable": true,
								"description": "Postal town, where the per-page company lookup returned one. Null on a row whose lookup missed, which is why `name` falls back to the feed’s own copy.",
								"type": "string"
							},
							"postal_code": {
								"nullable": true,
								"description": "Postal code, subject to the same lookup as `city`.",
								"type": "string"
							},
							"aira_url": {
								"description": "Link to the suggested company in Aira, for a person to open. The preview page, since by construction you do not hold it.",
								"type": "string"
							}
						},
						"required": [
							"id",
							"org_number",
							"name",
							"country",
							"city",
							"postal_code"
						],
						"description": "The suggested company. Fewer fields than `CompanySummary`, and no `portfolio_entry_id`: by construction you do not hold it."
					},
					"similar_to": {
						"type": "object",
						"properties": {
							"company_id": {
								"type": "string",
								"description": "The portfolio company the suggestion resembles, prefixed `cmp_`."
							},
							"portfolio_entry_id": {
								"nullable": true,
								"description": "Your entry for that company, prefixed `pfe_`, which is what you need to reach your own record of it. Null only when the company left your portfolio between the suggestion being written and this read.",
								"type": "string"
							},
							"name": {
								"nullable": true,
								"description": "Name of the portfolio company the suggestion came from.",
								"type": "string"
							},
							"aira_url": {
								"description": "Link to that portfolio company in Aira, for a person to open.",
								"type": "string"
							}
						},
						"required": [
							"company_id",
							"portfolio_entry_id",
							"name"
						],
						"description": "The portfolio company this suggestion was drawn from, which is what makes the row actionable."
					},
					"reason": {
						"nullable": true,
						"description": "Why the two companies resemble each other, in prose. Populated on effectively every row. There is no numeric score on this resource.",
						"type": "string"
					},
					"surfaced_at": {
						"type": "string",
						"description": "When this suggestion became available to **you**, which is the later of when it was computed and when you started tracking the company it came from. Not a freshness signal: nothing re-checks a suggestion after it is written.",
						"format": "date-time"
					}
				},
				"required": [
					"company",
					"similar_to",
					"reason",
					"surfaced_at"
				]
			},
			"Quotas": {
				"type": "object",
				"properties": {
					"quotas": {
						"type": "object",
						"properties": {
							"portfolio_companies": {
								"type": "object",
								"properties": {
									"used": {
										"type": "number",
										"description": "What you are using now, counted against `limit`. Never exceeds it: `used === limit` means the allowance is spent."
									},
									"limit": {
										"nullable": true,
										"description": "How much this allowance permits, or null if it is unmetered.",
										"type": "number"
									}
								},
								"required": [
									"used",
									"limit"
								],
								"description": "How many companies your plan lets you hold, and how many you hold now. Counts every company in your portfolio whatever added it: this API, the Aira app, a CRM import. Removing a company frees its slot immediately — but does not undo what the add wrote into a connected CRM."
							},
							"research_jobs": {
								"description": "Deep research runs in flight right now, and how many this API may have running at once. A slot frees when a run ends, not on a clock, and the count includes runs started anywhere — this API, the Aira app, a colleague. Runs started in the app are not bound by this limit, so `used === limit` means only that this API will refuse the next submission.",
								"type": "object",
								"properties": {
									"used": {
										"type": "number",
										"description": "What you are using now, counted against `limit`. Never exceeds it: `used === limit` means the allowance is spent."
									},
									"limit": {
										"nullable": true,
										"description": "How much this allowance permits, or null if it is unmetered.",
										"type": "number"
									}
								},
								"required": [
									"used",
									"limit"
								]
							}
						},
						"required": [
							"portfolio_companies"
						],
						"description": "Your allowances, keyed by what each one meters. New keys are added additively."
					}
				},
				"required": [
					"quotas"
				]
			},
			"ResearchJob": {
				"type": "object",
				"properties": {
					"id": {
						"type": "string",
						"description": "The job id (`rsr_…`)."
					},
					"status": {
						"type": "string",
						"enum": [
							"queued",
							"running",
							"completed",
							"failed"
						],
						"description": "`queued` and `running` mean the job is in flight; `completed` and `failed` are terminal. A `failed` job is never retried on your behalf."
					},
					"company": {
						"type": "string",
						"description": "The portfolio entry this research is about (`pfe_…`)."
					},
					"question": {
						"type": "string",
						"description": "The question you submitted, echoed back."
					},
					"title": {
						"nullable": true,
						"description": "A short title for the report. Null until it completes.",
						"type": "string"
					},
					"markdown": {
						"nullable": true,
						"description": "The report, as markdown prose. Null until the job completes. Companies named in the body appear in `mentioned_companies`.",
						"type": "string"
					},
					"sources": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"url": {
									"type": "string",
									"description": "The source URL."
								},
								"title": {
									"type": "string",
									"description": "The source’s own title."
								}
							},
							"required": [
								"url",
								"title"
							]
						},
						"description": "What the report drew on. Empty until the job completes."
					},
					"mentioned_companies": {
						"type": "array",
						"items": {
							"type": "object",
							"properties": {
								"id": {
									"type": "string",
									"description": "The company id (`cmp_…`). Resolve it with `GET /v1/companies/{id}`."
								},
								"name": {
									"type": "string",
									"description": "The company name as the report referred to it."
								},
								"country": {
									"nullable": true,
									"description": "ISO 3166-1 alpha-2 country code.",
									"type": "string"
								}
							},
							"required": [
								"id",
								"name",
								"country"
							]
						},
						"description": "Companies named in the report that we could resolve. Empty until the job completes, and empty when it named none we recognise."
					},
					"created_at": {
						"type": "string",
						"description": "When the job was submitted.",
						"format": "date-time"
					},
					"completed_at": {
						"nullable": true,
						"description": "When the job finished, successfully or not. Null while it is in flight.",
						"format": "date-time",
						"type": "string"
					},
					"failure": {
						"nullable": true,
						"description": "Present only on a `failed` job; null in every other state.",
						"type": "object",
						"properties": {
							"code": {
								"type": "string",
								"enum": [
									"cost_limit_reached",
									"research_unavailable",
									"research_timeout"
								],
								"description": "Why the job failed, as a stable value to branch on. Published here rather than as a response status because the request that produced it succeeded: the failure is a fact about the job, not about the call that fetched it."
							},
							"message": {
								"type": "string",
								"description": "A human-readable explanation of the failure, written for a developer reading a log. Do not match on it."
							}
						},
						"required": [
							"code",
							"message"
						]
					}
				},
				"required": [
					"id",
					"status",
					"company",
					"question",
					"title",
					"markdown",
					"sources",
					"mentioned_companies",
					"created_at",
					"completed_at",
					"failure"
				]
			},
			"submitResearchRequest": {
				"type": "object",
				"properties": {
					"company": {
						"type": "string",
						"description": "The portfolio entry to research (`pfe_…`). Companies must already be in your portfolio; this endpoint never adds one."
					},
					"question": {
						"type": "string",
						"minLength": 1,
						"maxLength": 1000,
						"description": "What you want to know, in plain language, up to 1000 characters. A specific question produces a better report than a broad one."
					}
				},
				"required": [
					"company",
					"question"
				]
			},
			"SavedView": {
				"type": "object",
				"properties": {
					"id": {
						"type": "string",
						"description": "Aira’s id for this view, prefixed `sav_`. Pass it to the read, update, delete and companies endpoints."
					},
					"name": {
						"type": "string",
						"description": "The view’s name as someone typed it, and the only thing here written for a human. Not unique: two views in one organization may share a name, so key on `id`."
					},
					"query": {
						"nullable": true,
						"description": "The view’s query as a public query tree, in the same shape `POST /v1/companies/search` accepts, so a view can be read, edited and written back. `null` when the view holds a filter the public query language cannot express (views created in the Aira app can); such a view is still readable and its companies still page normally.",
						"allOf": [
							{
								"$ref": "#/components/schemas/CompanyQuery"
							}
						]
					},
					"visibility": {
						"description": "`organization` — everyone here sees this view, and it may be driving a nightly CRM sync. `personal` — it belongs to one person and nobody else can see it.",
						"type": "string",
						"enum": [
							"organization",
							"personal"
						]
					},
					"created_at": {
						"type": "string",
						"description": "When the view was created.",
						"format": "date-time"
					},
					"updated_at": {
						"type": "string",
						"description": "When the view’s name or query last changed. It does not move when the companies the view matches change, because nothing is stored: membership is resolved on every read.",
						"format": "date-time"
					}
				},
				"required": [
					"id",
					"name",
					"query",
					"created_at",
					"updated_at"
				]
			},
			"createSavedViewRequest": {
				"type": "object",
				"properties": {
					"name": {
						"type": "string",
						"minLength": 1,
						"maxLength": 200,
						"description": "A name for the view, 1 to 200 characters, trimmed. Shown to every user in your organization in the Aira app. Not unique: two views may share a name."
					},
					"query": {
						"$ref": "#/components/schemas/CompanyQuery"
					}
				},
				"required": [
					"name",
					"query"
				]
			},
			"updateSavedViewRequest": {
				"type": "object",
				"properties": {
					"name": {
						"type": "string",
						"minLength": 1,
						"maxLength": 200,
						"description": "A name for the view, 1 to 200 characters, trimmed. Shown to every user in your organization in the Aira app. Not unique: two views may share a name."
					},
					"query": {
						"allOf": [
							{
								"$ref": "#/components/schemas/CompanyQuery"
							}
						]
					}
				}
			}
		}
	},
	"paths": {
		"/v1/me": {
			"get": {
				"operationId": "getMe",
				"summary": "Retrieve the organization the API key belongs to",
				"description": "Returns the organization that the supplied API key authenticates. Useful as a credential check: a 200 means the key is live, and a 401 means it is missing, revoked, expired, or unknown.\n\n**Key on `id`, not on `organization_name`.** The id is stable for the lifetime of the organization; the name changes when the organization renames itself.\n\nThis call is free, so it is safe to use as a health check.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/Organization"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_request.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				}
			}
		},
		"/v1/companies/search": {
			"post": {
				"operationId": "searchCompanies",
				"summary": "Search companies",
				"description": "Search the company universe with a boolean query tree and walk the results to completion with a cursor.\n\n**Two ways to write a query.** `query_string` takes the same query as `query`, written out. It compiles to exactly the tree `query` would have carried, so the two are interchangeable — including across a page boundary, where a cursor issued for one continues the other. Send one or the other, never both.\n\n```\ncountry = \"SE\"\n  AND revenue >= 50000000\n  AND (employees >= 25 OR revenue_growth_pct > 40)\n  AND NOT status = \"bankruptcy\"\n```\n\n**Strings are always quoted**, with `\"` or `'`. That is the one rule to remember, and it is what makes a bare word unambiguous: `revenue > profit` compares two fields rather than matching the text \"profit\". (Field-against-field is not executable yet and is refused, naming itself — but the quoting rule holds regardless, so `country = SE` is an error and `country = \"SE\"` is the query.) Numbers are bare and may group digits with `_`: `50_000_000`.\n\n**`AND`, `OR` and `NOT`** join conditions, `NOT` binding tightest and `OR` loosest — `country = \"SE\" AND revenue >= 1000000 OR employees >= 500` matches anything with 500 staff regardless of country. Parenthesise anything you would rather not have to check. Keywords are case-insensitive, and `&&`, `||` and `!` are accepted for them.\n\n**A list** is `[…]`. Use it with `IN` — `id IN [\"cmp_SE.1.5560000241\", \"cmp_SE.1.5560000242\"]` — and with the fields whose **Accepts a list** column says yes, where it means \"any of these\" without an operator change: `nace_code = [\"62\", \"63\"]`.\n\n**Operators.** The field table below names each one as it appears in `query`; this is how the same operator is written in `query_string`.\n\n| In `query` | In `query_string` |\n| --- | --- |\n| `eq` | `=` |\n| `neq` | `!=` |\n| `gt` | `>` |\n| `gte` | `>=` |\n| `lt` | `<` |\n| `lte` | `<=` |\n| `starts_with` | `STARTS WITH` |\n| `contains` | `CONTAINS` |\n| `in` | `IN` |\n\nA refusal carries the same code and the same node path as it would for `query`, plus `details.offset` — the character in your string the refusal is about.\n\n**Query shape.** A node is either a predicate, `{\"field\": \"revenue\", \"operator\": \"gte\", \"value\": 1000000}`, or a branch: `{\"and\": [...]}`, `{\"or\": [...]}`, `{\"not\": <node>}`. Exactly one key per node; a node carrying two is rejected rather than guessed at. Operators: eq, neq, gt, gte, lt, lte, starts_with, contains, in.\n\n**Filterable fields.** A field accepting a list treats `[\"a\",\"b\"]` as \"any of these\"; the others take a single value, and a range is two predicates. An operator a field does not support is a `query_invalid`, which names the ones it does. A field whose **Read back as** column says \"filter only\" is not returned on any resource: filter on it, but do not expect to read it back.\n\n| Field | Operators | Accepts a list | Read back as | Notes |\n| --- | --- | --- | --- | --- |\n| `cash_and_bank` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | `cash_and_bank` | Cash and bank balances, local currency, full units. |\n| `city` | `eq`, `neq`, `starts_with` | yes | `city` | Registered postal town. `starts_with` needs at least 2 characters. Coverage is uneven by market; see the `city` field on the company resource. |\n| `company_form` | `eq`, `neq` | no | `company_form` | Legal form, normalized across markets: `limited_company`, `one_man_company`, `non_profit`, `other`, `foreign_company`, `hbkb`, `foundation`, `housing_association`, `municipality`, `cooperative`, `religious_unit`, `governmental_unit`, `county`. `housing_association` is a Swedish BRF or Norwegian borettslag. Naming `foreign_company` (branch offices) lifts the branch-office defaults. |\n| `country` | `eq` | yes | `country` | ISO 3166-1 alpha-2 country code, and only a market Aira covers. One country per search: a query naming two is rejected. |\n| `ebitda` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | `ebitda` | EBITDA, local currency, full units. |\n| `ebitda_margin_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | filter only | EBITDA margin, percent. |\n| `employee_growth_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | filter only | Year-over-year headcount change, percent. |\n| `employees` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | `employees` | Headcount. Filterable everywhere, including the DE and DK companies whose record returns `employees: null` and a band: the filter reads the stored figure behind the band, matching on overlap. So a company can match a range its record does not show. |\n| `equity` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | `total_equity` | Total equity, local currency, full units. The company record calls the same figure `total_equity`. |\n| `equity_ratio_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | filter only | Equity ratio, percent. |\n| `id` | `eq`, `in` | yes | `id` | Company id. Accepts a single `cmp_` id (`eq`) or a batch (`in`, up to 500). Naming companies by id suppresses the product defaults, so a branch office or a dissolved company addressed by id is returned. |\n| `industry_tags` | `eq`, `neq` | yes | `industry_tags` | Our derived industry tags, from a closed vocabulary — an unknown value is refused and the refusal lists every valid tag. Populated on only a few per cent of companies, so a query filtering on them returns a small fraction of the companies that actually match: useful as a positive filter, misleading as a negative one, and the wrong tool for an industry that has a `nace_code` prefix. |\n| `nace_code` | `eq`, `starts_with` | yes | `nace_code` | NACE / SNI industry code, almost always stored WITHOUT separators (`6202`, not `62.02`) and at a depth that varies by registry: one activity is filed as 4 characters in some markets and 5 in others, sometimes with a letter suffix — computer consultancy appears as `6202`, `62020` and `6202A`. So `starts_with` is a raw prefix with two consequences: a dotted value matches almost nothing, and a prefix longer than 2 characters excludes the other spellings of the same activity. Prefix on the 2-character division (`62` for IT services) and filter the result further, rather than naming a precise code and reading a short answer as a small industry. A dotted form exists on a few thousand records out of tens of millions; reach those with `eq` on the exact value, never with a dotted prefix. |\n| `name` | `eq`, `starts_with`, `contains` | no | `name` | Company name. `starts_with` and `contains` need at least 3 characters. |\n| `org_number` | `eq` | no | `org_number` | National registration number, exact match. |\n| `postal_code` | `eq`, `starts_with`, `gte`, `lte` | yes | `postal_code` | Postal code. `starts_with` compiles to a prefix range, so a partial code such as `RG` selects a whole postal area. |\n| `profit` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | `profit` | Profit, local currency, full units. |\n| `profit_margin_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | `profit_margin_pct` | Profit margin, percent. |\n| `region` | `eq`, `neq`, `starts_with` | yes | filter only | Administrative region. Unpopulated in NO, DK, FI, GB, DE, BE, IE and LU. `starts_with` needs at least 2 characters. |\n| `revenue` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | `revenue` | Revenue in the company’s own reporting currency, full units. Includes modelled revenue where we hold no filed figure; the company record separates the two into `revenue` and `estimated_revenue`. |\n| `revenue_growth_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | `revenue_growth_pct` | Year-over-year revenue change, percent. |\n| `revenue_growth_streak` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | filter only | Consecutive years of revenue growth. |\n| `size_tier` | `eq`, `neq` | yes | `size_tier` | EU SME size tier (2003/361/EC): `micro`, `small`, `medium` or `large`. Null on a company we cannot tier, which is the majority, so this is a positive filter rather than a way to partition the corpus. |\n| `status` | `eq`, `neq` | yes | `status` | Registry status. `active` and `inactive` account for effectively the whole corpus; the rest are `bankruptcy`, `bankruptcy_completed`, `bankruptcy_ongoing`, `fusion_ongoing`, `liquidated`, `liquidation_ongoing`, `merged`, `reconstruction_ongoing`, `shell_corporation`, `other` and `unknown`. Defaults to `active` when your query does not mention it; naming it overrides that default. |\n| `website` | `eq`, `starts_with` | no | `website` | Primary website domain, without scheme (e.g. `volvo.com`). |\n| `year_founded` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | filter only | Year of incorporation. |\n\n**Limits**, validated before the search runs: at most 2 levels of nesting, 32 predicates per query, 8 branches per boolean node, and 500 values per `in` list. Exceeding one returns `query_too_complex` naming the limit and the offending node.\n\n**One country per search.** A query naming two countries is rejected; run one search per market.\n\n**Defaults.** Unless your query addresses them, results are limited to active headquarters, excluding branch offices. Name `status` to override the first; set `include_branch_offices` to lift the other. Naming a company by `id` or `org_number` suppresses all of them, so a branch office or a dissolved company addressed by id is returned. `meta.defaults_applied` reports what was applied.\n\n**Pagination.** Rows are ordered by `id` ascending, which is stable: every matching company appears exactly once across a page sequence, however deep. `limit` defaults to 50 and may not exceed 99. Pass `meta.next_cursor` back as `cursor` with the **same** query. A cursor presented with a different query is rejected: a page boundary only means something inside the result set it came from.\n\n**Rows are `CompanySummary`.** The full `Company` record is returned when you add a company to your portfolio, and by `GET /v1/companies/{id}` for companies you already hold.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/CompanySummary"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor",
												"query",
												"defaults_applied",
												"ordering"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												},
												"query": {
													"description": "The query as validated, echoed back so a caller can confirm what was understood."
												},
												"defaults_applied": {
													"type": "array",
													"items": {
														"type": "string"
													}
												},
												"ordering": {
													"type": "string",
													"enum": [
														"id_ascending"
													]
												}
											}
										}
									}
								},
								"examples": {
									"page": {
										"summary": "One page of results, with a cursor for the next",
										"value": {
											"data": [
												{
													"id": "cmp_SE.1.5560001234",
													"portfolio_entry_id": "pfe_1f04a0c8-7e2b-6a90-9f3d-2c8b41d5e7a6",
													"org_number": "556000-1234",
													"name": "Nordvik Industri AB",
													"country": "SE",
													"city": "Västerås",
													"postal_code": "72130",
													"aira_url": "https://ai.aira.app/app/companies/1f04a0c8-7e2b-6a90-9f3d-2c8b41d5e7a6"
												},
												{
													"id": "cmp_SE.1.5569876543",
													"portfolio_entry_id": null,
													"org_number": "556987-6543",
													"name": "Hedlund Verktyg AB",
													"country": "SE",
													"city": "Jönköping",
													"postal_code": "55302",
													"aira_url": "https://ai.aira.app/app/companies-preview/SE.1.5569876543?name=Hedlund%20Verktyg%20AB"
												}
											],
											"meta": {
												"next_cursor": "eyJxdWVyeSI6IjJmNGE5YzhlIiwia2V5IjoiU0UuMS41NTYwMDAxMjM0In0",
												"query": {
													"and": [
														{
															"field": "country",
															"operator": "eq",
															"value": "SE"
														},
														{
															"field": "revenue",
															"operator": "gte",
															"value": 50000000
														}
													]
												},
												"defaults_applied": [
													"status = active",
													"branch offices excluded"
												],
												"ordering": "id_ascending"
											}
										}
									},
									"exhausted": {
										"summary": "The last page: `next_cursor` is null",
										"value": {
											"data": [
												{
													"id": "cmp_SE.1.5569876543",
													"portfolio_entry_id": null,
													"org_number": "556987-6543",
													"name": "Hedlund Verktyg AB",
													"country": "SE",
													"city": "Jönköping",
													"postal_code": "55302",
													"aira_url": "https://ai.aira.app/app/companies-preview/SE.1.5569876543?name=Hedlund%20Verktyg%20AB"
												}
											],
											"meta": {
												"next_cursor": null,
												"query": {
													"field": "org_number",
													"operator": "eq",
													"value": "556000-1234"
												},
												"defaults_applied": [],
												"ordering": "id_ascending"
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum, query_invalid, query_too_complex.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"504": {
						"description": "Error. Possible `error.code` values: search_timeout.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/searchCompaniesRequest"
							},
							"example": {
								"query": {
									"and": [
										{
											"field": "country",
											"operator": "eq",
											"value": "SE"
										},
										{
											"field": "revenue",
											"operator": "gte",
											"value": 50000000
										},
										{
											"field": "employees",
											"operator": "gte",
											"value": 25
										}
									]
								},
								"limit": 25
							}
						}
					}
				}
			}
		},
		"/v1/companies": {
			"get": {
				"operationId": "listCompanies",
				"summary": "List the companies in your portfolio",
				"description": "Returns the companies your organization holds, newest entry first, as full `PortfolioCompany` records.\n\n**Membership is `approved` companies that have not been removed.** A company still in Aira’s review queue is not returned: it is our guess at what you might want, not something you hold.\n\n**Ordering is by when each company entered your portfolio**, newest first, and that is not the same as when its record was created. A company added through the API enters the moment you add it; one that arrived by email sync or a CRM import may sit in review for weeks and enter the day somebody approves it. `meta.ordering` is `entered_descending`.\n\n**Polling.** Send `entered_after` with the `meta.as_of` from your previous response to get only what has entered since. Echo the server’s watermark rather than your own clock: a row entered while your last request was in flight falls between the two, and nothing would tell you it was missed. A poll that matches nothing returns an empty page and **costs no company records**.\n\n**Pagination.** `limit` defaults to 50 and may not exceed 99. Pass `meta.next_cursor` back as `cursor` to continue. A cursor is bound to the parameters it was issued for; change any of them mid-walk and you get `invalid_cursor` rather than a page silently missing rows.\n\n**Counting.** `count_only: true` answers how many companies you hold and returns no rows — free, it spends no allowance. `data` is then an empty array and `meta.count` holds the answer, so read `meta.count` rather than the length of `data`. A cursor may not be combined with it.\n\n**Some companies have no `id`.** A company that reached your portfolio through email sync or a CRM import may have no match in our company data. On those rows `id` is null and every field that comes from that match — financials, addresses, directors, descriptions, industry codes — is null or empty, while your own `name`, `org_number` and `country` are still returned. `portfolio_entry_id` is present on **every** row, so use it as the stable key.\n\n**Filtering, in one field.** `q` takes an AQL condition on `name` — `q=name CONTAINS \"volvo\"`, also `=` and `STARTS WITH`, case-insensitive. Naming any other field is refused rather than ignored, and the refusal lists what this endpoint takes. Narrowing by country, industry or size is not available here: those come from company data, which roughly a third of portfolio rows have no match in, so a filter on them would quietly return a short answer. `POST /v1/companies/search` filters on all of them over the wider company universe rather than over your portfolio.\n\n**Additions only.** A company that leaves your portfolio simply stops appearing here; there is no removal event and no tombstone. If you mirror this list, reconcile against a full walk rather than assuming every change arrives as an addition.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/PortfolioCompany"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor",
												"ordering",
												"as_of"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												},
												"ordering": {
													"type": "string",
													"enum": [
														"entered_descending"
													]
												},
												"as_of": {
													"type": "string",
													"description": "The instant this page was read at, RFC 3339. Send it back as `entered_after` on your next poll: it is the only watermark that cannot miss a company entered while this request was in flight.",
													"format": "date-time"
												},
												"count": {
													"description": "How many companies your organization holds, when `count_only` is set. Absent otherwise — a page of rows does not report a total.",
													"type": "number"
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum, query_invalid, query_too_complex.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 99.",
							"maximum": 99,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					},
					{
						"name": "entered_after",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only companies that entered your portfolio strictly after this instant, RFC 3339. Use the `meta.as_of` from your previous response.",
							"format": "date-time",
							"type": "string",
							"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
						}
					},
					{
						"name": "q",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Narrow to companies whose name matches, as an AQL condition: `name CONTAINS \"volvo\"`, `name STARTS WITH \"volvo\"` or `name = \"Volvo Personvagnar AB\"`. Matching ignores case. Applies to `count_only` as well as to the rows, so the count and the pages always agree.",
							"type": "string"
						}
					},
					{
						"name": "count_only",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only how many companies you hold, with no rows. Free — it spends no company-record allowance. Cannot be combined with `cursor`.",
							"type": "string",
							"enum": [
								"true",
								"false"
							]
						}
					}
				]
			}
		},
		"/v1/companies/{id}": {
			"get": {
				"operationId": "getCompany",
				"summary": "Retrieve a company",
				"description": "Returns a company by its `cmp_` id.\n\n**The response shape depends on whether your organization holds the company.** If it is in your portfolio you get the full record: financials, filed statements, addresses, directors and descriptions. If it is not, you get the same eight-field summary that search returns. Add the company to your portfolio to get the full record.\n\n**`portfolio_entry_id` is the discriminator.** It is non-null exactly when the response is the full `Company` record, and null exactly when it is a `CompanySummary`, so branch on it rather than probing for the presence of a field. The two shapes are published as an `anyOf`.\n\nAn id that does not exist, and one that is not visible to your organization, both return `not_found`.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"anyOf": [
												{
													"$ref": "#/components/schemas/Company"
												},
												{
													"$ref": "#/components/schemas/CompanySummary"
												}
											]
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								},
								"examples": {
									"inPortfolio": {
										"summary": "A company in your portfolio: the full record",
										"value": {
											"data": {
												"id": "cmp_SE.1.5560001234",
												"portfolio_entry_id": "pfe_1f04a0c8-7e2b-6a90-9f3d-2c8b41d5e7a6",
												"org_number": "556000-1234",
												"name": "Nordvik Industri AB",
												"country": "SE",
												"city": "Västerås",
												"postal_code": "72130",
												"aira_url": "https://ai.aira.app/app/companies/1f04a0c8-7e2b-6a90-9f3d-2c8b41d5e7a6",
												"status": "active",
												"company_form": "limited_company",
												"registration_date": "1994-06-14",
												"vat_number": "SE556000123401",
												"nace_code": "2829",
												"sni_code": "28290",
												"industry_tags": [
													"Manufacturing"
												],
												"size_tier": "medium",
												"website": "example.com",
												"domains": [
													{
														"domain": "example.com",
														"source": "company_record",
														"primary": true
													},
													{
														"domain": "example.se",
														"source": "web_match",
														"primary": false
													}
												],
												"phone": "+46 8 123 45 67",
												"employees": 142,
												"employee_band": null,
												"revenue": 486200000,
												"estimated_revenue": null,
												"currency": "SEK",
												"ebitda": 61400000,
												"profit": 38900000,
												"total_equity": 214700000,
												"cash_and_bank": 52300000,
												"profit_margin_pct": 8,
												"revenue_growth_pct": 11.4,
												"financials": [
													{
														"period_end": "2025-12-31",
														"currency": "SEK",
														"revenue": 486200000,
														"operating_profit": 49100000,
														"profit_before_tax": 47800000,
														"net_profit": 38900000,
														"ebitda": 61400000,
														"total_equity": 214700000,
														"employees": 142
													},
													{
														"period_end": "2024-12-31",
														"currency": "SEK",
														"revenue": 436400000,
														"operating_profit": 41900000,
														"profit_before_tax": 40200000,
														"net_profit": 32100000,
														"ebitda": 53800000,
														"total_equity": 188400000,
														"employees": 131
													}
												],
												"description": "Designs and manufactures precision components for industrial machinery.",
												"description_long": "Nordvik Industri designs and manufactures precision components for industrial machinery, with a focus on hydraulic assemblies for the forestry and mining sectors. Founded in 1994, the company operates two production sites in Sweden and sells across the Nordics and northern Europe.",
												"last_scraped_at": "2026-07-28T04:12:00.000Z",
												"activity_text": "Bolaget ska bedriva tillverkning och försäljning av precisionskomponenter samt därmed förenlig verksamhet.",
												"addresses": [
													{
														"type": "post",
														"street": "Verkstadsgatan 12",
														"city": "Västerås",
														"postal_code": "72130",
														"country": "SE",
														"latitude": 59.6099,
														"longitude": 16.5448
													},
													{
														"type": "registered",
														"street": "Verkstadsgatan 12",
														"city": "Västerås",
														"postal_code": "72130",
														"country": "SE",
														"latitude": null,
														"longitude": null
													}
												],
												"directors": [
													{
														"name": "Anna Lindqvist",
														"role": "Verkställande direktör",
														"role_start_date": "2019-04-01"
													},
													{
														"name": "Erik Sandström",
														"role": "Styrelseordförande",
														"role_start_date": "2015-05-20"
													}
												],
												"owner": {
													"name": "Karin Öberg",
													"email": "karin.oberg@example.com"
												}
											},
											"meta": {}
										}
									},
									"notInPortfolio": {
										"summary": "A company you do not hold: the eight-field summary",
										"value": {
											"data": {
												"id": "cmp_SE.1.5569876543",
												"portfolio_entry_id": null,
												"org_number": "556987-6543",
												"name": "Hedlund Verktyg AB",
												"country": "SE",
												"city": "Jönköping",
												"postal_code": "55302",
												"aira_url": "https://ai.aira.app/app/companies-preview/SE.1.5569876543?name=Hedlund%20Verktyg%20AB"
											},
											"meta": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_id_prefix, invalid_request.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `cmp_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					}
				]
			}
		},
		"/v1/companies/{id}/contacts": {
			"get": {
				"operationId": "listCompanyContacts",
				"summary": "List a company’s contacts",
				"description": "Returns the people we hold for a company in your portfolio: name, title, email, phone and, where we have one, a LinkedIn profile URL. `linkedin_url` is best effort and is null more often than not; there is no LinkedIn data behind it beyond the link itself.\n\n**The company must be in your portfolio.** A company you do not hold returns `company_not_in_portfolio`, a refusal rather than an empty list, so you can tell \"not entitled\" from \"nothing on file\". Add the company to your portfolio to read its contacts.\n\n**An empty array is the normal answer.** Contacts are sparsely populated, and emails more sparsely than names. `data: []` means we hold nothing for that company; it is not an error and not a sign that something failed.\n\n**Fields can be individually absent.** A contact with a name and title but no email is common: registry-sourced directors carry names and titles only. A null `email`, `phone` or `linkedin_url` means we have nothing we can give you for that field, and you should not infer anything further from it.\n\n**Pagination.** Ordered stably, `limit` defaults to 50 and may not exceed 500. Pass `meta.next_cursor` back as `cursor` to continue. A cursor is bound to the company it came from; presenting it on a different company is rejected.\n\n**`meta.incomplete`** is true in the rare case that we hold more contacts for a company than this endpoint returns. When it is true, walking to a null `next_cursor` **does not** mean you have every contact we hold. It is false for effectively every company.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/Contact"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor",
												"incomplete"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												},
												"incomplete": {
													"type": "boolean",
													"description": "True when we hold more contacts for this company than this endpoint returns, so a null `next_cursor` does not mean you have them all. False for effectively every company."
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_id_prefix, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"403": {
						"description": "Error. Possible `error.code` values: company_not_in_portfolio.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `cmp_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 500.",
							"maximum": 500,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					}
				]
			}
		},
		"/v1/companies/{id}/filings": {
			"get": {
				"operationId": "listCompanyFilings",
				"summary": "List a company’s filings",
				"description": "Official registry filings for one company in your portfolio, newest first.\n\n**The company must be in your portfolio.** A company you do not hold returns `not_found`, the same answer as an id that does not exist. Filings are a portfolio-only resource, so there is no filings collection at that address either way.\n\n**Coverage is Sweden and Norway only.** Norwegian history runs back to 2016; **Swedish history begins 2025-09-09**. A company in any other market returns an empty list, and no field on the response distinguishes \"not covered\" from \"no recent changes\". For a GB or DE company, expect an empty list permanently.\n\n**An empty list is a normal answer.** It means we hold no filings for that company in the requested window, not that anything failed.\n\n**Filters and pagination** behave exactly as on `GET /v1/filings`: `type` takes comma-separated published values, `filed_since` is an absolute `YYYY-MM-DD` date, `limit` defaults to 50 and caps at 500, and a cursor is bound to both the company and the query that issued it.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/Filing"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_id_prefix, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable, filings_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `cmp_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 500.",
							"maximum": 500,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					},
					{
						"name": "type",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Filter by change type: one or more published values, comma-separated (e.g. `board_changed,company_name_changed`). Expands to every underlying registry code, including the alternative spellings a registry uses for the same event. `other` is not filterable.",
							"type": "string"
						}
					},
					{
						"name": "filed_since",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only filings recorded on or after this date (`YYYY-MM-DD`). Filings are date-only; there is no time component.",
							"type": "string",
							"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
						}
					}
				]
			}
		},
		"/v1/companies/{id}/news": {
			"get": {
				"operationId": "listCompanyNews",
				"summary": "List one company’s news",
				"description": "Returns news for a single company in your portfolio, newest first. Same items, grouping and pagination as `GET /v1/news`, narrowed to one company.\n\n**The company must be in your portfolio and approved.** Anything else is `not_found`: this endpoint answers for your portfolio, and a company you do not hold has no feed here. A company still awaiting review is not yet in your portfolio for this purpose.\n\n**`events[]`** carries the business events we extract from each article, which are the signals worth automating on.\n\n**No filters are applied by default.** Every language and the entire history, unless you pass `language`, `from` or `to`.\n\n**Pagination.** `limit` defaults to 50 and may not exceed 500. Walk `meta.next_cursor` until it is null rather than stopping on a short page. A cursor is bound to both the company and the filters it was issued with.\n\n**On `503 news_unavailable`,** wait for `Retry-After` and repeat the request with the **same** cursor rather than advancing past it.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/NewsItem"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_id_prefix, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable, news_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `cmp_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 500.",
							"maximum": 500,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					},
					{
						"name": "language",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only articles in this language, as a two-letter lowercase ISO 639-1 code (e.g. `sv`). Omit for every language; there is no default. A code we hold no articles for returns an empty page rather than an error, so check the spelling if a filtered feed comes back empty.",
							"type": "string",
							"pattern": "^[a-z]{2}$"
						}
					},
					{
						"name": "from",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only articles published on or after this date (`YYYY-MM-DD`). Omit for no lower bound; there is no default age cutoff.",
							"type": "string",
							"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
						}
					},
					{
						"name": "to",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only articles published on or before this date (`YYYY-MM-DD`). Omit for no upper bound.",
							"type": "string",
							"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
						}
					}
				]
			}
		},
		"/v1/news": {
			"get": {
				"operationId": "listNews",
				"summary": "List news for your portfolio",
				"description": "Returns news for the companies in your portfolio, newest first: press coverage, and anything the companies published on their own sites.\n\n**One story, not one article per outlet.** Coverage of the same event is grouped, and you receive the representative article. `story_id` is the stable key for that event and is shared by later re-covers, so it is what you join on to avoid processing the same news twice. It is null for a minority of items that have not been grouped, so handle that case rather than assuming a key.\n\n**`events[]` is the part worth building on.** These are business events we extract from the article (funding rounds, acquisitions, layoffs, distress), each with a date, a confidence and a short summary. They are the signals a CRM automation or an alerting rule keys on.\n\n**`companies[]` contains only your own portfolio entries.** An article mentioning a dozen companies lists just the ones you hold, with both the company id and your portfolio entry id.\n\n**No filters are applied by default.** Every language and the entire history, unless you pass `language`, `from` or `to`.\n\n**Pagination.** `limit` defaults to 50 and may not exceed 500. Pass `meta.next_cursor` back as `cursor` to continue, and keep going until it is null. **Do not stop on a short page:** a page can come back shorter than `limit` while more news remains. A cursor is bound to the filters it was issued with, so changing `language`, `from` or `to` mid-walk is rejected rather than silently resumed at a meaningless position.\n\n**On `503 news_unavailable`,** wait for `Retry-After` and repeat the request with the **same** cursor. It is a refusal, not an empty page, and the articles behind it are still there. Never advance a cursor past an error.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/NewsItem"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable, news_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 500.",
							"maximum": 500,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					},
					{
						"name": "language",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only articles in this language, as a two-letter lowercase ISO 639-1 code (e.g. `sv`). Omit for every language; there is no default. A code we hold no articles for returns an empty page rather than an error, so check the spelling if a filtered feed comes back empty.",
							"type": "string",
							"pattern": "^[a-z]{2}$"
						}
					},
					{
						"name": "from",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only articles published on or after this date (`YYYY-MM-DD`). Omit for no lower bound; there is no default age cutoff.",
							"type": "string",
							"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
						}
					},
					{
						"name": "to",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only articles published on or before this date (`YYYY-MM-DD`). Omit for no upper bound.",
							"type": "string",
							"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
						}
					}
				]
			}
		},
		"/v1/portfolio/companies": {
			"post": {
				"operationId": "addPortfolioCompanies",
				"summary": "Add companies to your portfolio",
				"description": "Adds up to 100 companies to your portfolio by `cmp_` id, and returns the full record for each one that lands.\n\n**This is the metered operation.** Your allowance is the number of companies you may **hold**, counted across every source: companies added here, and companies matched in from your CRM. It is not a monthly budget, so nothing resets, and removing a company frees its slot immediately. `meta.quotas.portfolio_companies` reports where you stand after every successful request, so an automation can slow down before it runs out rather than after.\n\n**Idempotent on the company id.** Re-adding a company you already hold costs nothing and returns `already_in_portfolio` with the same record. A batch that timed out can be retried verbatim: you are charged only for the companies that had not landed.\n\n**Partial success is the normal case.** The response is `200` whenever the batch was processed, including when every item was refused, and each entry in `data` carries its own `outcome`:\n\n| `outcome` | Meaning | Retry? |\n| --- | --- | --- |\n| `added` | The company is now in your portfolio. | n/a |\n| `already_in_portfolio` | It was already there. Not charged. | n/a |\n| `blocked` | Refused by an allowance or a limit. `error.code` says which. | No. Retrying changes nothing until the limit does. |\n| `failed` | Unknown id, or an error on our side. | Yes. |\n\n**Match on `company_id`, not on position.** Duplicate ids in one request collapse to a single entry, so the response may be shorter than the request.\n\nA request we cannot process at all is a `4xx` and charges nothing: more than 100 ids, an empty list, or an id carrying the wrong prefix (`invalid_id_prefix`) rejects the whole batch before anything is metered.\n\n**Added companies are enrolled in monitoring** and, if your organization has a CRM connected, synced to it. They start with the `lead` relationship status, exactly as a company added in the app does, but with **no owner**: an API key is not a person, so nothing is assumed about who the company belongs to, and the company is visible to your whole team rather than to one person.\n\nA full 100-company batch does real work per company, and can take tens of seconds. Size your batches for the timeout your client is comfortable with.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/PortfolioAddResult"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"quotas"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Always null here. This operation returns one entry per company in your request rather than a page of a larger collection, so there is nothing to continue. It is present only so that every collection response has the same `meta` shape."
												},
												"quotas": {
													"type": "object",
													"properties": {
														"portfolio_companies": {
															"type": "object",
															"properties": {
																"used": {
																	"type": "number",
																	"description": "What you are using now, counted against `limit`. Never exceeds it: `used === limit` means the allowance is spent."
																},
																"limit": {
																	"nullable": true,
																	"description": "How much this allowance permits, or null if it is unmetered.",
																	"type": "number"
																}
															},
															"required": [
																"used",
																"limit"
															]
														}
													},
													"required": [
														"portfolio_companies"
													]
												}
											}
										}
									}
								},
								"examples": {
									"mixedBatch": {
										"summary": "A partially refused batch, which is still a 200",
										"value": {
											"data": [
												{
													"company_id": "cmp_SE.1.5560001234",
													"outcome": "added",
													"portfolio_entry_id": "pfe_1f04a0c8-7e2b-6a90-9f3d-2c8b41d5e7a6",
													"company": {
														"id": "cmp_SE.1.5560001234",
														"portfolio_entry_id": "pfe_1f04a0c8-7e2b-6a90-9f3d-2c8b41d5e7a6",
														"org_number": "556000-1234",
														"name": "Nordvik Industri AB",
														"country": "SE",
														"city": "Västerås",
														"postal_code": "72130",
														"aira_url": "https://ai.aira.app/app/companies/1f04a0c8-7e2b-6a90-9f3d-2c8b41d5e7a6",
														"status": "active",
														"company_form": "limited_company",
														"registration_date": "1994-06-14",
														"vat_number": "SE556000123401",
														"nace_code": "2829",
														"sni_code": "28290",
														"industry_tags": [
															"Manufacturing"
														],
														"size_tier": "medium",
														"website": "example.com",
														"domains": [
															{
																"domain": "example.com",
																"source": "company_record",
																"primary": true
															},
															{
																"domain": "example.se",
																"source": "web_match",
																"primary": false
															}
														],
														"phone": "+46 8 123 45 67",
														"employees": 142,
														"employee_band": null,
														"revenue": 486200000,
														"estimated_revenue": null,
														"currency": "SEK",
														"ebitda": 61400000,
														"profit": 38900000,
														"total_equity": 214700000,
														"cash_and_bank": 52300000,
														"profit_margin_pct": 8,
														"revenue_growth_pct": 11.4,
														"financials": [
															{
																"period_end": "2025-12-31",
																"currency": "SEK",
																"revenue": 486200000,
																"operating_profit": 49100000,
																"profit_before_tax": 47800000,
																"net_profit": 38900000,
																"ebitda": 61400000,
																"total_equity": 214700000,
																"employees": 142
															},
															{
																"period_end": "2024-12-31",
																"currency": "SEK",
																"revenue": 436400000,
																"operating_profit": 41900000,
																"profit_before_tax": 40200000,
																"net_profit": 32100000,
																"ebitda": 53800000,
																"total_equity": 188400000,
																"employees": 131
															}
														],
														"description": "Designs and manufactures precision components for industrial machinery.",
														"description_long": "Nordvik Industri designs and manufactures precision components for industrial machinery, with a focus on hydraulic assemblies for the forestry and mining sectors. Founded in 1994, the company operates two production sites in Sweden and sells across the Nordics and northern Europe.",
														"last_scraped_at": "2026-07-28T04:12:00.000Z",
														"activity_text": "Bolaget ska bedriva tillverkning och försäljning av precisionskomponenter samt därmed förenlig verksamhet.",
														"addresses": [
															{
																"type": "post",
																"street": "Verkstadsgatan 12",
																"city": "Västerås",
																"postal_code": "72130",
																"country": "SE",
																"latitude": 59.6099,
																"longitude": 16.5448
															},
															{
																"type": "registered",
																"street": "Verkstadsgatan 12",
																"city": "Västerås",
																"postal_code": "72130",
																"country": "SE",
																"latitude": null,
																"longitude": null
															}
														],
														"directors": [
															{
																"name": "Anna Lindqvist",
																"role": "Verkställande direktör",
																"role_start_date": "2019-04-01"
															},
															{
																"name": "Erik Sandström",
																"role": "Styrelseordförande",
																"role_start_date": "2015-05-20"
															}
														],
														"owner": {
															"name": "Karin Öberg",
															"email": "karin.oberg@example.com"
														}
													},
													"error": null
												},
												{
													"company_id": "cmp_SE.1.5560009999",
													"outcome": "already_in_portfolio",
													"portfolio_entry_id": "pfe_1f04a0c8-91d4-6b71-8c0e-5a7f39b2c1d8",
													"company": null,
													"error": null
												},
												{
													"company_id": "cmp_SE.1.5560007777",
													"outcome": "blocked",
													"portfolio_entry_id": null,
													"company": null,
													"error": {
														"code": "portfolio_add_limit_reached",
														"message": "Your organization already holds as many companies as its plan allows. Remove companies you no longer follow, or contact your account manager to raise the number.",
														"details": {
															"quotas": {
																"portfolio_companies": {
																	"used": 2500,
																	"limit": 2500
																}
															}
														}
													}
												},
												{
													"company_id": "cmp_SE.1.5560005555",
													"outcome": "failed",
													"portfolio_entry_id": null,
													"company": null,
													"error": {
														"code": "not_found",
														"message": "No company with that id.",
														"details": null
													}
												}
											],
											"meta": {
												"quotas": {
													"portfolio_companies": {
														"used": 2500,
														"limit": 2500
													}
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_id_prefix, invalid_request.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"403": {
						"description": "Error. Possible `error.code` values: capability_not_granted, origin_not_allowed.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/addPortfolioCompaniesRequest"
							}
						}
					}
				}
			}
		},
		"/v1/filings": {
			"get": {
				"operationId": "listFilings",
				"summary": "List filings for your portfolio",
				"description": "Official registry filings for the companies in your portfolio, newest first.\n\n**Coverage is Sweden and Norway only.** These are national business-registry changes. Norwegian history runs back to 2016; **Swedish history begins 2025-09-09**. A company in any other country, or a Swedish filing from before that date, returns nothing. There is no coverage field on the response, so an empty feed for an uncovered company looks exactly like a company with no recent changes. If you hold companies outside SE and NO, expect them never to appear here.\n\n**Every filing belongs to a company in your portfolio.** Filings for companies you do not hold are not returned, and `company.portfolio_entry_id` is always present.\n\n**`type` is a stable published value, never a raw source code.** Some registry changes arrive under two different codes for the same event, so filtering on a source code would silently miss half of them. We publish one value per underlying change type and expand it back to every code behind it when you filter.\n\n**The set of types mirrors the registries, so it is wide and not ranked.** `board_changed` and `share_capital_changed` sit in the same list as `fax_changed` and `phone_removed`. Decide which types matter to you and filter on them rather than reading the whole feed. `other` is the bucket for change types we have not named; it cannot be used as a `type` filter.\n\n**Filters.** `type` takes one or more published values, comma-separated. `filed_since` is an absolute `YYYY-MM-DD` date and returns filings recorded on or after it.\n\n**Pagination.** `limit` defaults to 50 and may not exceed 500. Pass `meta.next_cursor` back as `cursor` to continue; a null `next_cursor` means the feed is exhausted. A cursor is bound to the query and the organization that issued it, so reusing it with different filters is rejected rather than silently reinterpreted. **A page may contain fewer rows than `limit`, or none at all, while `next_cursor` is still non-null**, so drive your loop on the cursor rather than on the row count.\n\n**An upstream failure is `filings_unavailable` (503), never an empty page.** Retry the same cursor. An empty page would make a paginating client advance past filings it never received.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/Filing"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable, filings_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 500.",
							"maximum": 500,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					},
					{
						"name": "type",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Filter by change type: one or more published values, comma-separated (e.g. `board_changed,company_name_changed`). Expands to every underlying registry code, including the alternative spellings a registry uses for the same event. `other` is not filterable.",
							"type": "string"
						}
					},
					{
						"name": "filed_since",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only filings recorded on or after this date (`YYYY-MM-DD`). Filings are date-only; there is no time component.",
							"type": "string",
							"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
						}
					}
				]
			}
		},
		"/v1/similar-companies": {
			"get": {
				"operationId": "listSimilarCompanies",
				"summary": "List similar companies",
				"description": "Companies that resemble ones already in your portfolio, newest first. Each row names the suggestion, the portfolio company it resembles, and why.\n\n**The feed is portfolio-seeded and excludes companies you already hold.** Every row is a company that is not in your portfolio, so `company` carries no `portfolio_entry_id`. `similar_to.portfolio_entry_id` is the one you can act on: it points at your own entry for the company the suggestion was drawn from.\n\n**Coverage is partial, and an empty feed is the normal answer.** Most of a portfolio contributes no lookalikes at all. An empty `data` array means we hold no suggestions for you; it is not an error, and it does not mean something failed.\n\n**Lookalikes are computed once per company rather than refreshed**, and coverage is partial. There is no freshness field on the row: `surfaced_at` is when a suggestion became available to you, not when it was last checked.\n\n**Every row cleared a similarity bar of 7 out of 10.** Candidates are rated 1 to 10 against the portfolio company and only those scoring 7 or above are stored. There is no score on the wire; `reason` is the explanation, and it is populated on effectively every row.\n\n**Dismissals in the Aira app do not shape this feed.** \"Not a fit\" is a personal action by one person, so it never removes a row from what this endpoint returns. Track what you have already reviewed on your side.\n\n**Pagination.** `limit` defaults to 50 and may not exceed 500. Pass `meta.next_cursor` back as `cursor` to continue; a null `next_cursor` means you have the whole feed. A cursor belongs to the collection it came from: never construct one, and never reuse one across organizations.\n\n**An upstream failure is a `503 similar_companies_unavailable`, never an empty page.** Retry the same request without advancing your cursor. An empty page is a normal answer on this feed, so an error is the only way to tell a failure from one.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/SimilarCompany"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable, similar_companies_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 500.",
							"maximum": 500,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					}
				]
			}
		},
		"/v1/quotas": {
			"get": {
				"operationId": "getQuotas",
				"summary": "Retrieve your current quota usage",
				"description": "Returns your portfolio allowance — how many companies you hold and how many your plan lets you hold — and how many deep research runs are in flight against the concurrency bound. An automation can call this before starting a run rather than finding out mid-batch.\n\n**Standing figures, not monthly ones.** `portfolio_companies.used` is how many companies are in your portfolio right now, whatever put them there, and removing one frees its slot immediately — though it does not undo what the add wrote into a connected CRM. `research_jobs.used` counts runs still `queued` or `running` anywhere in the organization, so a slot frees when one ends. Nothing resets on a date, so there is no period to wait for and no `period_start` or `period_end` on either entry.\n\n`limit: null` means that allowance is unmetered; `used` still reports your real usage. Otherwise `used` never exceeds `limit`, and `used === limit` means the allowance is spent.\n\n**Two allowances are not reported here, and neither is readable in advance.** Both announce themselves through their own error code rather than through a number you can plan against:\n\n- **Requests per minute**, in the `X-RateLimit-*` headers on every response, and `rate_limit_exceeded` when you pass it.\n- **A monitored-company ceiling**, configured separately from your plan allowance and set on very few organizations. It is not reported anywhere on this API; if it applies to you, an add returns `monitored_company_limit_reached`.\n\nMetered operations report the same entry shape in `meta.quotas` after the fact, so one parser handles either. A response never carries both: reading your usage consumes nothing, so this one has an empty `meta`.\n\nThis call is free: it consumes nothing against any allowance, including the one it reports.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/Quotas"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_request.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				}
			}
		},
		"/v1/research": {
			"post": {
				"operationId": "submitResearch",
				"summary": "Submit a deep research job",
				"description": "Starts a deep research run against a company in your portfolio and returns the job. Poll `GET /v1/research/{id}` until `status` is `completed` or `failed`.\n\n**A research call spends real money.** There is no sandbox, no test key and no validation mode: your first call runs for real against your organization’s AI spending limit. Decide what you want to ask before you call this.\n\n**A run takes up to ten minutes.** Poll on the order of tens of seconds, not hundreds of milliseconds. `queued` and `running` mean the job is in flight; `completed` and `failed` are terminal.\n\n**Resubmitting the same question is safe.** An identical question for the same company, while the first is still in flight, returns `200` with the job that already exists rather than starting a second paid run. Questions are compared trimmed and case-insensitively. A client whose request timed out can send it again.\n\n**`failed` is final.** We do not retry a failed job for you, so nothing arrives later to contradict what you recorded. `failure.code` says whether resubmitting could help: `research_unavailable` and `research_timeout` are worth another try, `cost_limit_reached` is not.\n\n**Up to five jobs may be in flight at once**, counting jobs started in the Aira app as well as through this API, and counting both `queued` and `running` ones. Over that, this returns `429 research_concurrency_exceeded`; list `GET /v1/research` to see which jobs are holding the slots rather than retrying on a timer.\n\nThe company must already be in your portfolio, named by its `pfe_` id. Passing a `cmp_` id returns `invalid_id_prefix`: adding a company is a metered operation with a recurring cost and a write into your CRM, so this endpoint will never do it for you. Add it with `POST /v1/portfolio/companies` first.\n\n**Fetch results and store what you need.** We do not guarantee that a report remains retrievable indefinitely.",
				"responses": {
					"200": {
						"description": "An identical research job was already in flight for this company, and is returned unchanged. Nothing was submitted and nothing was charged.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/ResearchJob"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"202": {
						"description": "Accepted. A new research job was created and is queued.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/ResearchJob"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_id_prefix, invalid_request.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"403": {
						"description": "Error. Possible `error.code` values: capability_not_granted, origin_not_allowed.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: research_concurrency_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/submitResearchRequest"
							},
							"example": {
								"company": "pfe_018f4a2b-7c31-6d90-a1e2-3f4b5c6d7e8f",
								"question": "Who are their main competitors in the Nordics, and how are they differentiated?"
							}
						}
					}
				}
			},
			"get": {
				"operationId": "listResearch",
				"summary": "List your deep research jobs",
				"description": "Returns your research jobs, newest first, so an integration that lost track of a job id can find its in-flight work rather than resubmitting it.\n\nFilter with `status` and `company`. At the concurrency ceiling, list without a `status` filter: both `queued` and `running` jobs hold a slot, so filtering to one of them hides half the answer.\n\n`cursor` is bound to the filters it was issued for. Changing `status` or `company` mid-walk returns `invalid_cursor` rather than a page silently missing rows. Start a new sequence with no cursor instead.\n\nPaginate with `meta.next_cursor`, echoed back as `cursor`. Default page size 50, maximum 500.\n\n**A page of completed reports counts against your daily company-record allowance**, for every company named in every report on the page. Filter by `status` when you are polling rather than listing everything, and store reports once you have fetched them.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/ResearchJob"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_id_prefix, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 500.",
							"maximum": 500,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					},
					{
						"name": "status",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only jobs in this state.",
							"type": "string",
							"enum": [
								"queued",
								"running",
								"completed",
								"failed"
							]
						}
					},
					{
						"name": "company",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Return only jobs about this portfolio entry (`pfe_…`).",
							"type": "string"
						}
					}
				]
			}
		},
		"/v1/research/{id}": {
			"get": {
				"operationId": "getResearch",
				"summary": "Retrieve a deep research job",
				"description": "Returns a research job and, once it has finished, the report.\n\nPoll this until `status` is `completed` or `failed`. A run takes up to ten minutes, so poll on the order of tens of seconds. While the job is in flight, `title`, `markdown` and `completed_at` are null and `sources` and `mentioned_companies` are empty. The fields are always present, so your parser does not need to branch on status.\n\n`failed` is terminal: we never retry a job on your behalf, so nothing arrives later to contradict what you recorded. `failure.code` tells you whether resubmitting could help.\n\n**Fetch the report and store what you need.** We do not guarantee that results remain retrievable indefinitely, and every company named in `mentioned_companies` counts against your daily company-record allowance each time you read the job, so poll until it is terminal and then stop.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/ResearchJob"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_id_prefix, invalid_request.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `rsr_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					}
				]
			}
		},
		"/v1/saved-views": {
			"get": {
				"operationId": "listSavedViews",
				"summary": "List saved views",
				"description": "Lists your organization’s saved views, newest first.\n\n**Organization views only.** A saved view created through this API belongs to the organization: every user in the Aira app can see it, and it survives the departure of whoever created it. Personal saved views that individual users create in the app are not part of this collection and cannot be read, changed or deleted with an API key.\n\n`query` uses the same query tree as `POST /v1/companies/search`: the same fields, operators and limits, validated the same way. A query this endpoint accepts is one that endpoint would run.\n\nA view created in the Aira app can hold a filter the public query language cannot express. Such a view reads back with `query: null` and its companies still page normally; it cannot be round-tripped through `PATCH`.\n\n**There is no member count.** A view’s companies are resolved live, so a count would mean running every view on every list. Read `GET /v1/saved-views/{id}/companies` for members.\n\n**Pagination.** `limit` defaults to 50 and may not exceed 500. Pass `meta.next_cursor` back as `cursor` to continue.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/SavedView"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 500.",
							"maximum": 500,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					}
				]
			},
			"post": {
				"operationId": "createSavedView",
				"summary": "Create a saved view",
				"description": "Creates an organization saved view from a name and a query.\n\n`query` uses the same query tree as `POST /v1/companies/search`: the same fields, operators and limits, validated the same way. A query this endpoint accepts is one that endpoint would run.\n\nA view created in the Aira app can hold a filter the public query language cannot express. Such a view reads back with `query: null` and its companies still page normally; it cannot be round-tripped through `PATCH`.\n\n**The query is validated now, not at read time.** A query that cannot be compiled is rejected here with `query_invalid` or `query_too_complex`, so a view that was created is a view whose companies will page.\n\n**Creating and holding a view is free.** It consumes no allowance: no companies are added. Only reading a view’s companies counts, exactly as any other company read does.\n\nThe view belongs to your organization and is visible to every user in the Aira app.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/SavedView"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_request, query_invalid, query_too_complex.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"403": {
						"description": "Error. Possible `error.code` values: capability_not_granted, origin_not_allowed.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/createSavedViewRequest"
							},
							"example": {
								"name": "Swedish mid-market manufacturers",
								"query": {
									"and": [
										{
											"field": "country",
											"operator": "eq",
											"value": "SE"
										},
										{
											"field": "revenue",
											"operator": "gte",
											"value": 50000000
										},
										{
											"field": "nace_code",
											"operator": "starts_with",
											"value": "25"
										}
									]
								}
							}
						}
					}
				}
			}
		},
		"/v1/saved-views/{id}": {
			"get": {
				"operationId": "getSavedView",
				"summary": "Retrieve a saved view",
				"description": "Returns one of your organization’s saved views.\n\n`query` uses the same query tree as `POST /v1/companies/search`: the same fields, operators and limits, validated the same way. A query this endpoint accepts is one that endpoint would run.\n\nA view created in the Aira app can hold a filter the public query language cannot express. Such a view reads back with `query: null` and its companies still page normally; it cannot be round-tripped through `PATCH`.\n\nA personal saved view belonging to a user in the Aira app returns `not_found`, the same as an id that names nothing.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/SavedView"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_id_prefix, invalid_request.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `sav_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					}
				]
			},
			"patch": {
				"operationId": "updateSavedView",
				"summary": "Rename a saved view or replace its query",
				"description": "Changes a saved view’s name, its query, or both. Omitted fields are left as they are.\n\n`query` uses the same query tree as `POST /v1/companies/search`: the same fields, operators and limits, validated the same way. A query this endpoint accepts is one that endpoint would run.\n\nA view created in the Aira app can hold a filter the public query language cannot express. Such a view reads back with `query: null` and its companies still page normally; it cannot be round-tripped through `PATCH`.\n\n**A view that is synced to your CRM nightly can only be given a query it can keep syncing.** If a user in the Aira app has turned on a nightly sync for this view, replacing its query re-checks the size of the list: too many companies for a nightly sync is `saved_view_sync_too_large`, no room left in your plan is `saved_view_sync_plan_limit_reached`, and a size we could not measure right now is `saved_view_size_unavailable`. In every case nothing is changed and the sync keeps running its previous query. Renaming is never affected, and neither is a view with no sync.\n\n**Replacing the query invalidates outstanding cursors** for that view’s companies. A pagination cursor identifies a position inside one result set, so continuing an old walk against a new query would silently skip or repeat companies; such a cursor is rejected with `invalid_cursor` instead. Start the walk again with no cursor.\n\nA personal saved view belonging to a user in the Aira app returns `not_found` and is left untouched.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/SavedView"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_id_prefix, invalid_request, query_invalid, query_too_complex, saved_view_sync_too_large.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"403": {
						"description": "Error. Possible `error.code` values: capability_not_granted, origin_not_allowed, saved_view_sync_plan_limit_reached.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable, saved_view_size_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `sav_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					}
				],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/updateSavedViewRequest"
							},
							"example": {
								"name": "Swedish mid-market manufacturers (revised)"
							}
						}
					}
				}
			},
			"delete": {
				"operationId": "deleteSavedView",
				"summary": "Delete a saved view",
				"description": "Deletes one of your organization’s saved views. The companies it matched are untouched: a saved view is a stored query, not a container.\n\nThe deleted view is returned, so a caller has a record of what was removed.\n\nDeleting is not idempotent in the sense of returning 200 twice: a view that is already gone returns `not_found`, as does a personal saved view belonging to a user in the Aira app.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"$ref": "#/components/schemas/SavedView"
										},
										"meta": {
											"type": "object",
											"description": "Reserved for response metadata. Empty today; fields are added additively.",
											"properties": {}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_id_prefix, invalid_request.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"403": {
						"description": "Error. Possible `error.code` values: capability_not_granted, origin_not_allowed.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `sav_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					}
				]
			}
		},
		"/v1/saved-views/{id}/companies": {
			"get": {
				"operationId": "listSavedViewCompanies",
				"summary": "List the companies in a saved view",
				"description": "Returns the companies that match a saved view’s query, as `CompanySummary` rows.\n\n**Membership is resolved live.** The view’s query runs on every request, so results reflect the company universe as it is now. A saved view is a stored query, not a stored list. Nothing is synced and nothing goes stale.\n\n**Rows are `CompanySummary`**, exactly as `POST /v1/companies/search` returns. Each row carries `id` (a `cmp_` id, always) and `portfolio_entry_id` (a `pfe_` id when your organization already holds the company, otherwise null), so one page tells you which members you have and which you do not. The full record is returned when you add a company to your portfolio.\n\n**Pagination.** Rows are ordered by `id` ascending, which is stable: every matching company appears exactly once across a page sequence, however deep. `limit` defaults to 50 and may not exceed 99. Pass `meta.next_cursor` back as `cursor` to continue.\n\n**A cursor belongs to one version of one view.** If the view’s query is replaced with `PATCH` while you are paging, outstanding cursors are rejected with `invalid_cursor` rather than resumed against the new query, which would silently skip or repeat companies. Start again with no cursor.\n\n**Product defaults apply here as they do to a search**: unless the view’s query addresses them, results are limited to active headquarters, excluding branch offices. `meta.defaults_applied` reports which were applied. A view created in the Aira app that filters on these itself suppresses them.\n\n**Reading members costs company reads.** Each row served counts against your allowance exactly as a search result does.\n\nA personal saved view belonging to a user in the Aira app returns `not_found`.",
				"responses": {
					"200": {
						"description": "Success",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": [
										"data",
										"meta"
									],
									"properties": {
										"data": {
											"type": "array",
											"items": {
												"$ref": "#/components/schemas/CompanySummary"
											}
										},
										"meta": {
											"type": "object",
											"required": [
												"next_cursor",
												"defaults_applied",
												"ordering"
											],
											"properties": {
												"next_cursor": {
													"type": "string",
													"nullable": true,
													"description": "Opaque cursor for the next page, or null when the collection is exhausted. Echo it back as `cursor`; do not construct it."
												},
												"defaults_applied": {
													"type": "array",
													"items": {
														"type": "string"
													}
												},
												"ordering": {
													"type": "string",
													"enum": [
														"id_ascending"
													]
												}
											}
										}
									}
								}
							}
						}
					},
					"400": {
						"description": "Error. Possible `error.code` values: invalid_cursor, invalid_id_prefix, invalid_request, limit_exceeds_maximum.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"401": {
						"description": "Error. Possible `error.code` values: invalid_api_key, invalid_token, missing_api_key.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"404": {
						"description": "Error. Possible `error.code` values: not_found.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"429": {
						"description": "Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							},
							"Retry-After": {
								"description": "Seconds to wait before retrying.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Limit": {
								"description": "Requests permitted per minute for this class of endpoint.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Remaining": {
								"description": "Requests left in the current window.",
								"schema": {
									"type": "integer"
								}
							},
							"X-RateLimit-Reset": {
								"description": "Unix timestamp (seconds) at which the current window resets.",
								"schema": {
									"type": "integer"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"500": {
						"description": "Error. Possible `error.code` values: internal_error.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"503": {
						"description": "Error. Possible `error.code` values: api_unavailable.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					},
					"504": {
						"description": "Error. Possible `error.code` values: search_timeout.",
						"headers": {
							"X-Request-Id": {
								"description": "Identifier for this request, mirroring `request_id` in error bodies. Log it: it is what turns a support thread into a lookup.",
								"schema": {
									"type": "string"
								}
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								}
							}
						}
					}
				},
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "Prefixed `sav_` id. Passing an id with a different prefix returns `invalid_id_prefix`.",
						"schema": {
							"type": "string"
						}
					},
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Rows per page. Defaults to 50, maximum 99.",
							"maximum": 99,
							"default": 50,
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "cursor",
						"in": "query",
						"required": false,
						"schema": {
							"description": "Echo `meta.next_cursor` from the previous page here. Opaque: never construct or parse one, and never reuse one across a different query or organization.",
							"type": "string"
						}
					}
				]
			}
		}
	}
}
