# Aira Connect API — complete reference Version 1.0.0. Generated from the OpenAPI document at https://developers.aira.app/openapi.json, which is itself generated from the running implementation. ## Overview The Aira Connect API is server-to-server only. Authenticate with 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. Browser 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. Collections are cursor-paginated. Every successful response is wrapped in `{ data, meta }`; every error is `{ error: { code, message }, request_id }`. Within `/v1` changes are additive only. Timestamps are ISO-8601 UTC and numbers are unformatted. ## Conventions Base URL: https://ai.aira.app/api - Authenticate with `Authorization: Bearer aira_live_…`. Keys are unscoped and are issued in Connect settings. - Every successful response is `{ "data": …, "meta": … }`. `meta` is present even on single resources. - Every error is `{ "error": { "code", "message", "details"? }, "request_id" }`. - `X-Request-Id` is returned on every response, success or error, and matches `request_id`. - Collections are cursor paginated: pass `cursor`, read `meta.next_cursor`, stop when it is null. Cursors are opaque, so do not construct them. - Timestamps are ISO-8601 UTC. Numbers are unformatted. - Ids are prefixed by resource kind. Passing an id with the wrong prefix returns `invalid_id_prefix` rather than a 404. ## Operations ### GET /v1/me operationId: `getMe` Retrieve the organization the API key belongs to 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. Response 200: `data` is a single Organization. Fields: - organization_name: string (required) Errors: - 400: Error. Possible `error.code` values: invalid_request. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 404: Error. Possible `error.code` values: not_found. - 429: Error. Possible `error.code` values: rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/me" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ### POST /v1/companies/search operationId: `searchCompanies` Search companies Search the company universe with a boolean query tree and walk the results to completion with a cursor. **Query shape.** A node is either a predicate — `{"field": "revenue", "operator": "gte", "value": 1000000}` — or a branch: `{"and": [...]}`, `{"or": [...]}`, `{"not": }`. 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. **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. | Field | Operators | Accepts a list | Notes | | --- | --- | --- | --- | | `cash_and_bank` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Cash and bank balances, local currency, full units. | | `city` | `eq`, `neq`, `starts_with` | yes | Registered postal town. `starts_with` needs at least 2 characters. Coverage is uneven by market — see the `city` field on the company resource. | | `country` | `eq` | yes | ISO 3166-1 alpha-2 country code. One country per search — a query naming two is rejected. | | `ebitda` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | EBITDA, local currency, full units. | | `ebitda_margin_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | EBITDA margin, percent. | | `employee_growth_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Year-over-year headcount change, percent. | | `employees` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Headcount. Banded source data in DE and DK; the band minimum is filtered. | | `equity` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Total equity, local currency, full units. | | `equity_ratio_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Equity ratio (solidity), percent. | | `id` | `eq`, `in` | yes | 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. | | `industry_tags` | `eq`, `neq` | yes | Our derived industry tags. Sparsely populated (~3.5% of companies) — useful as a positive filter, misleading as a negative one. | | `nace_code` | `eq`, `starts_with` | yes | NACE / SNI industry code. `starts_with` selects a branch of the taxonomy and needs at least 2 characters. | | `name` | `eq`, `starts_with`, `contains` | no | Company name. `starts_with` and `contains` need at least 3 characters. | | `org_number` | `eq` | no | National registration number, exact match. | | `postal_code` | `eq`, `starts_with`, `gte`, `lte` | yes | Postal code. `starts_with` compiles to a prefix range, so a partial code such as `RG` selects a whole postal area. | | `profit` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Profit, local currency, full units. | | `profit_margin_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Profit margin, percent. | | `region` | `eq`, `neq`, `starts_with` | yes | Administrative region. Unpopulated in NO, DK, FI, GB, DE, BE, IE and LU. `starts_with` needs at least 2 characters. | | `revenue` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Revenue in the company’s own reporting currency, full units. Includes modelled revenue where we hold no filed figure — the company resource separates the two into `revenue` and `estimated_revenue`. | | `revenue_growth_pct` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Year-over-year revenue change, percent. | | `revenue_growth_streak` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Consecutive years of revenue growth. | | `size_tier` | `eq`, `neq` | yes | EU SME size tier (2003/361/EC). | | `status` | `eq`, `neq` | yes | Registry status, e.g. `active`. Defaults to `active` when you do not mention it; naming it overrides that default. | | `website` | `eq`, `starts_with` | no | Primary website domain, without scheme (e.g. `volvo.com`). | | `year_founded` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | no | Year of incorporation. | **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. **One country per search.** A query naming two countries is rejected; run one search per market. Every other field accepts an array of values as "any of these". **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. **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, because a page boundary only means anything within the result set it came from. **Rows are `company_summary`.** The full record is returned when you add a company to your portfolio, and by `GET /v1/companies/{id}` for companies you already hold. Request body (application/json): - query: object (required) - limit: integer - cursor: string - include_branch_offices: boolean Response 200: `data` is an array of CompanySummary. Fields: - id: string (required) - portfolio_entry_id: string? (required) - org_number: string? (required) - name: string? (required) - country: string? (required) - city: string? (required) - postal_code: string? (required) Errors: - 400: Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum, query_invalid, query_too_complex. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 429: Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. - 504: Error. Possible `error.code` values: search_timeout. Example: ```bash curl -X POST "https://ai.aira.app/api/v1/companies/search" \ -H "Authorization: Bearer aira_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "query": { "and": [ { "field": "country", "operator": "eq", "value": "SE" }, { "field": "revenue", "operator": "gte", "value": 50000000 }, { "field": "employees", "operator": "gte", "value": 25 } ] }, "limit": 25 }' ``` ### GET /v1/companies/{id} operationId: `getCompany` Retrieve a company Returns a company by its `cmp_` id. **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, descriptions. If it is not, you get the same seven-field summary that search returns. Add the company to your portfolio to get the full record. **`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 `anyOf` rather than `oneOf` deliberately: `Company` is a superset of `CompanySummary`, and the component schemas stay open so that fields can be added additively within `/v1`. An id that does not exist, and one that is not visible to your organization, both return `not_found`. Parameters: - id (path, string) (required). Prefixed `cmp_` id. Passing an id with a different prefix returns `invalid_id_prefix`. Response 200: `data` is a single Company | CompanySummary. Fields (`Company`): - id: string (required) - portfolio_entry_id: string? (required) - org_number: string? (required) - name: string? (required) - country: string? (required) - city: string? (required) - postal_code: string? (required) - status: string? (required) - company_form: string? (required) - registration_date: string? (required) - vat_number: string? (required) - nace_code: string? (required) - sni_code: string? (required) - industry_tags: array (required) - size_tier: string? (required) - website: string? (required) - domains: array (required) - phone: string? (required) - employees: number? (required) - employee_band: object? (required) - revenue: number? (required) - estimated_revenue: object? (required) - currency: string? (required) - ebitda: number? (required) - profit: number? (required) - total_equity: number? (required) - cash_and_bank: number? (required) - profit_margin_pct: number? (required) - revenue_growth_pct: number? (required) - financials: array (required) - description: string? (required) - description_long: string? (required) - last_scraped_at: string? (required) - activity_text: string? (required) - addresses: array (required) - directors: array (required) Fields (`CompanySummary`): - id: string (required) - portfolio_entry_id: string? (required) - org_number: string? (required) - name: string? (required) - country: string? (required) - city: string? (required) - postal_code: string? (required) Errors: - 400: Error. Possible `error.code` values: invalid_id_prefix, invalid_request. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 404: Error. Possible `error.code` values: not_found. - 429: Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/companies/cmp_example" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ### GET /v1/companies/{id}/contacts operationId: `listCompanyContacts` List a company’s contacts Returns the people we hold for a company in your portfolio: name, title, email, phone and LinkedIn. **The company must be in your portfolio.** A company you do not hold returns `company_not_in_portfolio` — a refusal, not an empty list, so you can tell "not entitled" from "nothing on file". Add the company to your portfolio to read its contacts. **An empty array is the normal answer.** Roughly a third of companies have any contact on file and a sixth have an email. `data: []` means we hold nothing for that company; it is not an error and not a sign that something failed. **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` or `phone` means we have nothing we can give you for that field, and you should not infer anything further from it. **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. **`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. Parameters: - id (path, string) (required). Prefixed `cmp_` id. Passing an id with a different prefix returns `invalid_id_prefix`. - limit (query, integer) - cursor (query, string) Response 200: `data` is an array of Contact. Fields: - name: string (required) - title: string? (required) - email: string? (required) - phone: string? (required) - linkedin_url: string? (required) Errors: - 400: Error. Possible `error.code` values: invalid_cursor, invalid_id_prefix, invalid_request, limit_exceeds_maximum. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 403: Error. Possible `error.code` values: company_not_in_portfolio. - 404: Error. Possible `error.code` values: not_found. - 429: Error. Possible `error.code` values: rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/companies/cmp_example/contacts" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ### GET /v1/companies/{id}/filings operationId: `listCompanyFilings` List a company’s filings Official registry filings for one company in your portfolio, newest first. **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. **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 there is no field on the response that distinguishes "not covered" from "no recent changes" — so for a GB or DE company, expect an empty list permanently. **An empty list is a normal answer.** It means we hold no filings for that company in the requested window, not that anything failed. **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. Parameters: - id (path, string) (required). Prefixed `cmp_` id. Passing an id with a different prefix returns `invalid_id_prefix`. - limit (query, integer) - cursor (query, string) - type (query, string) - filed_since (query, string) Response 200: `data` is an array of Filing. Fields: - id: string (required). Stable identifier for this filing. - company: object (required) - filed_on: string (required). The date the registry recorded the change, as `YYYY-MM-DD`. Date-only: the source carries no time component. - type: 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 | 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) (required). What changed, as a stable published value. Never a raw Creditsafe code. - description: string? (required). The registry’s own English description of the change. Errors: - 400: Error. Possible `error.code` values: invalid_cursor, invalid_id_prefix, invalid_request, limit_exceeds_maximum. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 404: Error. Possible `error.code` values: not_found. - 429: Error. Possible `error.code` values: rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. - 503: Error. Possible `error.code` values: filings_unavailable. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/companies/cmp_example/filings" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ### GET /v1/companies/{id}/news operationId: `listCompanyNews` List one company’s news Returns news for a single company in your portfolio, newest first. Same items, grouping and pagination as `GET /v1/news`, narrowed to one company. **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. **`events[]`** carries the business events we extract from each article — the signals worth automating on. **No filters are applied by default.** Every language and the entire history, unless you pass `language`, `from` or `to`. **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. **On `503 news_unavailable`,** wait for `Retry-After` and repeat the request with the SAME cursor rather than advancing past it. Parameters: - id (path, string) (required). Prefixed `cmp_` id. Passing an id with a different prefix returns `invalid_id_prefix`. - limit (query, integer) - cursor (query, string) - language (query, string) - from (query, string) - to (query, string) Response 200: `data` is an array of NewsItem. Fields: - id: string (required) - story_id: string? (required) - title: string (required) - url: string (required) - published_at: string (required) - language: string? (required) - source: enum(media | company_website) (required). `media` for third-party press coverage, `company_website` for something the company published about itself. - companies: array (required) - events: array (required) Errors: - 400: Error. Possible `error.code` values: invalid_cursor, invalid_id_prefix, invalid_request, limit_exceeds_maximum. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 404: Error. Possible `error.code` values: not_found. - 429: Error. Possible `error.code` values: rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. - 503: Error. Possible `error.code` values: news_unavailable. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/companies/cmp_example/news" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ### GET /v1/news operationId: `listNews` List news for your portfolio Returns news for the companies in your portfolio, newest first — press coverage and anything the companies published on their own sites. **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. **`events[]` is the part worth building on.** These are business events we extract from the article — funding rounds, acquisitions, layoffs, distress — with a date, a confidence and a short summary. They are the signals a CRM automation or an alerting rule keys on. **`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. **No filters are applied by default.** Every language and the entire history, unless you pass `language`, `from` or `to`. **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: pages can come back shorter than `limit` while more news remains, because items for companies you have not approved are removed after the page is read. A cursor is bound to the filters it was issued with — changing `language`, `from` or `to` mid-walk is rejected rather than silently resumed at a meaningless position. **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. Parameters: - limit (query, integer) - cursor (query, string) - language (query, string) - from (query, string) - to (query, string) Response 200: `data` is an array of NewsItem. Fields: - id: string (required) - story_id: string? (required) - title: string (required) - url: string (required) - published_at: string (required) - language: string? (required) - source: enum(media | company_website) (required). `media` for third-party press coverage, `company_website` for something the company published about itself. - companies: array (required) - events: array (required) Errors: - 400: Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 429: Error. Possible `error.code` values: rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. - 503: Error. Possible `error.code` values: news_unavailable. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/news" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ### POST /v1/portfolio/companies operationId: `addPortfolioCompanies` Add companies to your portfolio Adds up to 100 companies to your portfolio by `cmp_` id, and returns the full record for each one that lands. **This is the metered operation.** Each distinct company added counts once against your allowance for the current period. `meta.quotas.portfolio_adds` reports where you stand after every successful request, so an automation can self-throttle instead of discovering the wall. **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. **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`: | `outcome` | Meaning | Retry? | | --- | --- | --- | | `added` | The company is now in your portfolio. | n/a | | `already_in_portfolio` | It was already there. Not charged. | n/a | | `blocked` | Refused by an allowance or a limit. `error.code` says which. | No. Retrying changes nothing until the limit does. | | `failed` | Unknown id, or an error on our side. | Yes. | **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. A 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. **Added companies are enrolled in monitoring** and, if your organization has a CRM connected, synced to it. They are owned by your API service user and start with the `lead` relationship status, exactly as a company added in the app does. A 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. Request body (application/json): - company_ids: array (required). Company ids (`cmp_…`) to add to your portfolio, 1 to 100 per request. Response 200: `data` is an array of PortfolioAddResult. Fields: - company_id: string (required) - outcome: enum(added | already_in_portfolio | blocked | failed) (required) - portfolio_entry_id: string? (required) - company: Company? (required) - error: object? (required) Errors: - 400: Error. Possible `error.code` values: invalid_id_prefix, invalid_request. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 403: Error. Possible `error.code` values: origin_not_allowed. - 429: Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. ### GET /v1/filings operationId: `listFilings` List filings for your portfolio Official registry filings for the companies in your portfolio, newest first. **Coverage is Sweden and Norway only.** These are national business-registry changes sourced from Creditsafe, and no other market is available. 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, and because there is no coverage field on the response, 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. **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. **`type` is a stable published value, never a raw Creditsafe code.** Some registry changes arrive under two different vendor codes for the same event — filtering on a vendor code would silently miss half of them, so we publish one value per real change type and expand it back to every underlying code when you filter. `other` is the bucket for change types we have not named; it cannot be used as a `type` filter. **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. **Pagination.** `limit` defaults to 50 and may not exceed 500; a request for exactly 500 returns up to 499 rows per page, which does not affect how many filings you can read in total. 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 — reusing it with different filters is rejected rather than silently reinterpreted. **A page may contain fewer rows than `limit` while `next_cursor` is still non-null**, so drive your loop on the cursor, not on the row count. **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. Parameters: - limit (query, integer) - cursor (query, string) - type (query, string) - filed_since (query, string) Response 200: `data` is an array of Filing. Fields: - id: string (required). Stable identifier for this filing. - company: object (required) - filed_on: string (required). The date the registry recorded the change, as `YYYY-MM-DD`. Date-only: the source carries no time component. - type: 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 | 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) (required). What changed, as a stable published value. Never a raw Creditsafe code. - description: string? (required). The registry’s own English description of the change. Errors: - 400: Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 429: Error. Possible `error.code` values: rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. - 503: Error. Possible `error.code` values: filings_unavailable. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/filings" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ### GET /v1/similar-companies operationId: `listSimilarCompanies` List similar companies Companies that resemble ones already in your portfolio, newest first. Each row names the suggestion, the portfolio company it resembles, and why. **The feed is portfolio-seeded and excludes companies you already hold.** Every row is a company that is not in your portfolio, so there is no `portfolio_entry_id` on `company` — it would be null on every row. `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. **Coverage is partial, and an empty feed is a normal answer.** Roughly a fifth of monitored companies have any lookalike at all, so most of a portfolio contributes nothing here. An empty `data` array means we hold no suggestions for you — it is not an error, and it does not mean something failed. **The corpus is frozen.** Lookalikes were last computed on 2026-07-03 and the pipeline is paused, so a company added since then will not gain suggestions while it stays that way. Lookalikes are also computed once per company rather than refreshed, so a restart would extend coverage to new companies rather than revisit the ones already covered. There is no freshness field on the row: `surfaced_at` is when a suggestion became available to you, not when it was last checked. **Every row cleared a similarity bar of 7 out of 10.** Candidates are rated against the portfolio company on a 1–10 scale and only those scoring 7 or above are stored. There is no score on the wire, because the rating is not retained past that check; `reason` is the explanation, and it is populated on effectively every row. **Dismissals in the Aira app do not shape this feed.** "Not a fit" is a personal action by one person in your organization, so it never removes a row from what this endpoint returns. Track what you have already reviewed on your side. **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. **An upstream failure is a `503 similar_companies_unavailable`, never an empty page.** Retry the same request without advancing your cursor. This distinction matters here more than anywhere else on this API, because an empty page is otherwise a perfectly ordinary answer. Parameters: - limit (query, integer) - cursor (query, string) Response 200: `data` is an array of SimilarCompany. Fields: - company: object (required) - similar_to: object (required) - reason: string? (required) - surfaced_at: string (required) Errors: - 400: Error. Possible `error.code` values: invalid_cursor, invalid_request, limit_exceeds_maximum. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 429: Error. Possible `error.code` values: daily_row_limit_exceeded, rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. - 503: Error. Possible `error.code` values: similar_companies_unavailable. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/similar-companies" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ### GET /v1/quotas operationId: `getQuotas` Retrieve your current quota usage Returns every quota on your account, so an automation can check its standing before starting work rather than discovering a limit mid-run. This is the read counterpart to `meta.quotas`, which appears on metered operations and reports only what that request consumed. The entry shape is identical in both places, so one parser handles either — but a response never carries both: reading your usage consumes nothing, so this response has an empty `meta`. `limit: null` means your plan is unmetered for that quota; `used` still reports real consumption. Otherwise `used` never exceeds `limit`, and `used === limit` means the allowance is spent. Reading your quotas is free: it consumes nothing against any allowance, including the one it reports. Response 200: `data` is a single Quotas. Fields: - quotas: object (required) Errors: - 400: Error. Possible `error.code` values: invalid_request. - 401: Error. Possible `error.code` values: invalid_api_key, missing_api_key. - 404: Error. Possible `error.code` values: not_found. - 429: Error. Possible `error.code` values: rate_limit_exceeded. - 500: Error. Possible `error.code` values: internal_error. Example: ```bash curl -X GET "https://ai.aira.app/api/v1/quotas" \ -H "Authorization: Bearer aira_live_your_key_here" ``` ## Error codes Every 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. Errors 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. | Code | HTTP status | Retryable | | --- | --- | --- | | `invalid_cursor` | 400 | No | | `invalid_id_prefix` | 400 | No | | `invalid_request` | 400 | No | | `limit_exceeds_maximum` | 400 | No | | `query_invalid` | 400 | No | | `query_too_complex` | 400 | No | | `invalid_api_key` | 401 | No | | `missing_api_key` | 401 | No | | `company_not_in_portfolio` | 403 | No | | `monitored_company_limit_reached` | 403 | No | | `origin_not_allowed` | 403 | No | | `portfolio_add_limit_reached` | 403 | No | | `not_found` | 404 | No | | `daily_row_limit_exceeded` | 429 | Yes | | `rate_limit_exceeded` | 429 | Yes | | `research_concurrency_exceeded` | 429 | Yes | | `internal_error` | 500 | Yes | | `api_unavailable` | 503 | Yes | | `filings_unavailable` | 503 | Yes | | `news_unavailable` | 503 | Yes | | `similar_companies_unavailable` | 503 | Yes | | `search_timeout` | 504 | Yes | ### invalid_cursor **HTTP 400** The `cursor` was not one this API issued, or is old enough that it can no longer be decoded. Echo `meta.next_cursor` back exactly as you received it, and never construct a cursor yourself. Start the collection from the beginning to recover. *Retryable:* No. Retrying the same request returns the same error. ### invalid_id_prefix **HTTP 400** An id was well formed but carried the wrong resource prefix, for example a `pfe_` id where a `cmp_` id belongs. Pass an id of the kind the endpoint documents. The prefix is there so you can tell at a glance which kind you are holding. *Retryable:* No. Retrying the same request returns the same error. ### invalid_request **HTTP 400** The request did not match the documented schema. Read `error.details` for the offending field paths, fix them and send it again. *Retryable:* No. Retrying the same request returns the same error. ### limit_exceeds_maximum **HTTP 400** The `limit` you asked for is above the maximum this endpoint accepts. Lower `limit` and walk the rest of the collection with `meta.next_cursor`. *Retryable:* No. Retrying the same request returns the same error. ### query_invalid **HTTP 400** A 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. `error.details` carries a JSON path to the node that failed. This is separate from `invalid_request` because the query tree is the part you iterate on. *Retryable:* No. Retrying the same request returns the same error. ### query_too_complex **HTTP 400** A search query went past a published complexity limit: nesting depth, total leaves, branches in a single `or`, or the length of an `in` list. `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. *Retryable:* No. Retrying the same request returns the same error. ### invalid_api_key **HTTP 401** The 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. Check you are sending the key you think you are, and that it has not been revoked. Issue a replacement in Connect settings. *Retryable:* No. Retrying the same request returns the same error. ### missing_api_key **HTTP 401** The request carried no `Authorization` header, or one this API could not parse. Send `Authorization: Bearer aira_live_…` using a key issued in Connect settings. *Retryable:* No. Retrying the same request returns the same error. ### company_not_in_portfolio **HTTP 403** The 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. Add 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. *Retryable:* No. Retrying the same request returns the same error. ### monitored_company_limit_reached **HTTP 403** Your organization is at its monitored-company limit, so this company was not added. Every company in your portfolio carries ongoing monitoring, and that ceiling is separate from your portfolio-add allowance. Reported per company inside a 200, alongside any companies that were added. Remove companies you no longer track, or contact your account manager to raise the limit. *Retryable:* No. Retrying the same request returns the same error. ### origin_not_allowed **HTTP 403** A state-changing request arrived with an `Origin` header, which means it came from a browser. Call this API from your server. A key that works in a browser is a key your users can read. *Retryable:* No. Retrying the same request returns the same error. ### portfolio_add_limit_reached **HTTP 403** Your organization has spent its portfolio-add allowance for the current period, so this company was not added. Quota counts distinct companies, so re-adding one you already hold never causes this. `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_adds` carries your usage against your allowance. Contact your account manager to raise it. *Retryable:* No. Retrying the same request returns the same error. ### not_found **HTTP 404** The resource does not exist, or is not visible to your organization. Check the id. This API does not distinguish a resource that never existed from one you cannot see. *Retryable:* No. Retrying the same request returns the same error. ### daily_row_limit_exceeded **HTTP 429** Your 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. Resume 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. *Retryable:* Yes. Wait for the interval in the `Retry-After` header, then retry. ### rate_limit_exceeded **HTTP 429** Your organization sent more requests per minute than this class of endpoint allows. The budget is shared by every key your organization holds, so issuing a second key does not double it. Wait the number of seconds in `Retry-After`, then continue. `X-RateLimit-Remaining` on every successful response tells you how much of the current window is left, so a well-behaved client never has to reach this. *Retryable:* Yes. Wait for the interval in the `Retry-After` header, then retry. ### research_concurrency_exceeded **HTTP 429** Your 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. Poll 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. *Retryable:* Yes. Wait for the interval in the `Retry-After` header, then retry. ### internal_error **HTTP 500** Something failed on our side. Retry once. If it persists, contact support and quote the `request_id` from the response body. *Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval. ### api_unavailable **HTTP 503** The API is switched off, either for your organization or for everyone. Your key is fine. A person flips this switch, so no `Retry-After` is sent: any number here would be invented. Try again later, and contact support with your `request_id` if it persists. *Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval. ### filings_unavailable **HTTP 503** The filings data source did not respond in time. Your request was valid and your cursor is still good. Retry 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. *Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval. ### news_unavailable **HTTP 503** We could not read the news corpus for your request. The upstream store timed out or answered with something we will not serve. 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. If you are walking a feed, resuming from the cursor you last succeeded on loses nothing. *Retryable:* Yes. Wait for the interval in the `Retry-After` header, then retry. ### similar_companies_unavailable **HTTP 503** The lookalike feed could not be read. This is an upstream failure, not a statement about your data. Retry 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 precisely so the two can be told apart. *Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval. ### search_timeout **HTTP 504** A search ran longer than the upstream budget allows. A broad query over a large market is the usual cause. Add filters to narrow the query, then try again. *Retryable:* Yes. Back off and try again. This response carries no `Retry-After` header, so pick your own interval. This 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. ## Testing your integration How to build against this API without a sandbox, and what to know before your first write. ### There is no sandbox Every key is a live key, prefixed `aira_live_`, and every call runs against your real organization. We did not build a sandbox on purpose: a pretend mode would have to run its own branch through the metering and idempotency logic, which is exactly the code you most want tested for real before you depend on it. What follows is how to exercise the API safely without one. ### Reads are free, so start there Reading 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. ```bash curl https://ai.aira.app/api/v1/me \ -H "Authorization: Bearer aira_live_your_key_here" ``` Company search and company detail are reads too. Iterate on your query shape as much as you like: the only cost is time. ### Adds are real > **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 would genuinely want in your portfolio. This is the one place where the absence of a sandbox actually costs you something, so it is worth agreeing internally which companies your integration test uses before you write it. A handful of well known companies you already track is the usual answer. ### How quota counts Quota counts distinct companies per period, not requests. Two consequences worth designing around: - 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. - 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. Retrying 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. ### Watch your usage while you build Check 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. > **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. ### Call it from your server This 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. Keys 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.