Catalog API
The Catalog API lets scripts and CI jobs do what the Catalog console does: declare services and keep them current, read the inventory, decide which service a resource belongs to, read drift and SLA reports, and export everything for an audit. Every endpoint, with its parameters, schemas and errors, is in the Catalog API reference. This page shows the common tasks and the rules that apply to all of them.
Paths below are relative to https://evohub.io, and every one starts with /api/v1/catalog/. Requests need an API key, sent as described in API overview.
Scopes
| To | Scope |
|---|---|
| Read services, tiers and SLA reports | catalog:component:read |
| Declare, change and delete services, set their RACI, manage tiers | catalog:component:write |
| Read resources, changes, applications, images and the map | catalog:resource:read |
| Accept and reject matches, put a resource or an application in a service | catalog:resource:write |
| Read environments and sources | catalog:source:read |
| Manage environments, rename, pause and resume sources | catalog:source:write |
| Read cloud accounts | identity:cloudaccount:read |
| Connect and manage cloud accounts | identity:cloudaccount:write (connecting also needs catalog:source:write) |
The match queue, drift, a service's resources and the inventory export need both catalog:component:read and catalog:resource:read. A key without a scope gets 403, never 401. See Catalog permissions.
Declare a service
curl https://evohub.io/api/v1/catalog/components \
-H "Authorization: Bearer $EVOHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "payments-api",
"display_name": "Payments API",
"owner_team_id": "team_3b6f0e2a-1c4d-4e8f-9a7b-2d5c6e8f0a1b",
"tier": 1,
"runbook_url": "https://wiki.acme.example/runbooks/payments"
}'
slug and display_name are required; the slug can never change. Every endpoint that takes a service accepts its ID (cmp_…) or its slug, so GET /api/v1/catalog/components/payments-api works. PATCH changes only the fields you send; send null to clear owner_team_id, tier or sla_overrides, and "" to clear a text field.
A pipeline can keep services in step with a repository: PATCH each one, and POST it when the answer is 404.
Find what nobody owns
curl "https://evohub.io/api/v1/catalog/resources?unowned=true&exclude_kind=Namespace,Node" \
-H "Authorization: Bearer $EVOHUB_API_KEY"
Lists page with cursor: pass the previous answer's pagination.next_cursor until has_more is false. The first page of the resource and application lists also carries pagination.total. Other useful filters: image=nginx (any tag of a repository), floating=true (images with no fixed version), component_id, environment_id and status.
GET /api/v1/catalog/drift returns the whole drift report in one answer.
Decide the match queue
# What is waiting
curl https://evohub.io/api/v1/catalog/matches \
-H "Authorization: Bearer $EVOHUB_API_KEY"
# Accept one
curl -X POST https://evohub.io/api/v1/catalog/matches/$MATCH_ID/accept \
-H "Authorization: Bearer $EVOHUB_API_KEY"
# Or put an application in a service directly
curl -X POST https://evohub.io/api/v1/catalog/applications/$APP_ID/component \
-H "Authorization: Bearer $EVOHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"component": "payments-api"}'
A rejected proposal is never proposed again. {"component": null} unbinds. See Matching and drift.
Read an SLA report
curl "https://evohub.io/api/v1/catalog/components/payments-api/sla-report?month=2026-10" \
-H "Authorization: Bearer $EVOHUB_API_KEY"
Without month, the current month so far. Each part's status is measured, not_connected or unavailable.
Export the inventory
GET /api/v1/catalog/exports/inventory returns every asset not gone, with its application, service, owner team, tier, availability target and RACI: at most 50,000 rows, with truncated when there were more. Teams and people are their IDs.
Cloud accounts
Connections are created first and given access second: POST /api/v1/catalog/connections returns the connection with its external_id; then, for AWS, download the role template from GET /api/v1/catalog/connections/{id}/template, create the role, and PATCH its role_arn. POST /api/v1/catalog/connections/{id}/check tries the access and answers {"ok": true, "account_id": "…"} or {"ok": false, "error": "…"}. See Cloud accounts.
Things to know
- Agents only. The routes under
/api/v1/catalog/ingest/are for the EvoHub agent, which signs each request with its own key. An API key cannot send inventory, and an agent cannot read. - Observations are read-only. Resources and applications are never created or edited through the API; only which service they belong to is decided here. Sources have no create or delete either: an agent's appears when it first reports, and is retired by revoking the agent.
- Parts you cannot read are left out. A service's detail omits
observationsfor a key withoutcatalog:resource:read; a resource's or application's omitsproposalswithoutcatalog:component:read; a cloud account'sexternal_idis empty withoutidentity:cloudaccount:write. - Strict bodies. A field an endpoint does not know is refused with
INVALID_BODY, so a typo never reads as success. - Errors. Branch on
error.code, for exampleNOT_FOUND,SLUG_TAKEN,SLUG_IMMUTABLE,MATCH_DECIDEDorENVIRONMENT_IN_USE. The general rules are in Errors.

