This page covers the endpoints that need more than the JSON:API Overview gives you: the per-application change proposal routes, the create payloads for use cases and test cases, the CI test-run upload, and the v2 context endpoint. Every request here needs a Bearer token; see API Authentication for how to get one.
The JSON:API routes (/api/v1/change-proposals, /api/v1/use-cases, /api/v1/test-cases) expect
Content-Type: application/vnd.api+json on POST and PATCH. Sending application/json returns
415. The two custom endpoints (POST /api/v1/test-runs and GET /api/v2/context) are plain JSON
and use their own error shape, described in their sections.
Change Proposals by Application
A change proposal is a suggested create, update, or connect against a catalog entity. Three list routes return the proposals for one application:
| Route | Returns | Order |
|---|---|---|
GET /api/v1/change-proposals/by-application |
Every proposal for the application | Oldest first (inserted_at) |
GET /api/v1/change-proposals/pending-by-application |
Not applied and not dismissed | Highest confidence_score first |
GET /api/v1/change-proposals/dismissed-by-application |
Dismissed and not applied | Most recently dismissed first |
Pass the application with filter[application_id]. It is required: leaving it out returns 400
with an error code of required. These routes are not paginated and return the full set.
curl "https://catalio.ai/api/v1/change-proposals/pending-by-application?filter[application_id]=0199a3e2-5b7c-7d10-9f2a-3c4d5e6f7a8b" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/vnd.api+json"
Response (200, attributes trimmed):
{
"data": [
{
"type": "ChangeProposal",
"id": "0199a3f0-1c2d-7e3f-8a4b-5c6d7e8f9a0b",
"attributes": {
"action_type": "create",
"target_type": "requirement",
"target_id": null,
"proposed_changes": {
"title": "Add OAuth2 Login",
"description": "Support OAuth2 authentication"
},
"change_rationale": "Identified during code analysis",
"source": "code_analysis",
"confidence_score": 0.85,
"application_id": "0199a3e2-5b7c-7d10-9f2a-3c4d5e6f7a8b",
"applied_at": null,
"dismissed_at": null,
"dismissal_reason": null,
"was_edited": false
}
}
]
}
An application in another organization is not an error: the request succeeds with an empty data
array, the same as an application with no proposals.
Values you’ll see in the enum attributes:
| Attribute | Values |
|---|---|
action_type |
create, update, connect |
source |
ai_discovery, document_extraction, meeting_extraction, manual, initiative_analysis, artifact_extraction, code_analysis, issue_extraction |
dismissal_reason |
not_relevant, inaccurate, duplicate, too_granular, already_covered, other |
gap_classification |
new, covered, conflict |
Other Change Proposal Routes
| Route | What it does |
|---|---|
GET /api/v1/change-proposals?initiative_id=<uuid> |
Proposals for one initiative, oldest first. initiative_id is required. |
GET /api/v1/change-proposals/:id |
One proposal |
POST /api/v1/change-proposals |
Create a proposal, with duplicate detection (below) |
PATCH /api/v1/change-proposals/:id |
Edit proposed_changes, change_rationale, confidence_score, or evidence |
PATCH /api/v1/change-proposals/:id/apply |
Mark the proposal applied. Fails if it is already applied. |
DELETE /api/v1/change-proposals/:id |
Delete a proposal. Fails if it is already applied. |
A proposal that has been applied can’t be edited, deleted, or applied again.
Creating a Change Proposal
curl -X POST https://catalio.ai/api/v1/change-proposals \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-d '{
"data": {
"type": "ChangeProposal",
"attributes": {
"action_type": "create",
"target_type": "requirement",
"target_id": null,
"proposed_changes": {
"title": "Add OAuth2 Login",
"description": "Support OAuth2 authentication"
},
"change_rationale": "Identified during code analysis",
"source": "code_analysis",
"confidence_score": 0.85,
"evidence": {"sources": ["main.py"]},
"application_id": "0199a3e2-5b7c-7d10-9f2a-3c4d5e6f7a8b"
}
}
}'
target_type, proposed_changes, change_rationale, and source are required. The rules that
catch people out:
target_idandsnapshot_beforemust benullwhenaction_typeiscreate.target_idis required whenaction_typeisupdate, and forconnect(except whentarget_typeisrequirement_relation).confidence_scoreis between0.0and1.0, and can’t exceed0.55whensourceismeeting_extraction.idempotency_key(optional, up to 255 characters) makes the create safe to retry: posting the same key again while the first proposal is still pending returns201with that same proposal instead of creating a new one, even when the application is at its intake limit.
Responses:
| Status | When |
|---|---|
201 |
Created. The body is the new ChangeProposal resource. |
409 |
A matching proposal already exists. errors[0].code is duplicate_proposal and errors[0].meta.existing_proposal_id is its ID. |
400 |
Validation failed, including the per-application intake limit on pending proposals (reported on application_id). |
503 |
The duplicate check couldn’t run. Retry the request. |
Without an idempotency_key, Catalio compares the proposal against existing ones by content. When it
finds a pending match it merges your evidence into that proposal and returns the 409 above, so treat
409 as “already recorded”, not as a failure.
Creating Use Cases
POST /api/v1/use-cases creates one use case (an acceptance criterion).
curl -X POST https://catalio.ai/api/v1/use-cases \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-d '{
"data": {
"type": "UseCase",
"attributes": {
"title": "Sign in with a valid password",
"type": "happy_path",
"format": "gherkin",
"details": {
"given_clause": "a registered user on the sign-in page",
"when_clause": "they submit a valid email and password",
"then_clause": "they land on their dashboard"
},
"validation_tag": "visual",
"priority": "high",
"requirement_id": "0199a3e5-2a3b-7c4d-9e5f-6a7b8c9d0e1f"
}
}
}'
Required. title always. type, format, and details are also required unless you set
discovery_method to scan; the default is manual.
details for format: "gherkin". given_clause, when_clause, and then_clause must each be
present and non-empty. feature and additional_steps are optional. Missing clauses fail with
Gherkin format requires given_clause, when_clause, then_clause to be present and non-empty (listing
whichever are missing).
requirement_id is optional. When you set it and the requirement has a requirement type, type
must fit that type, or the create fails with an error on type that lists the allowed values:
| Requirement type | Allowed use case type |
|---|---|
data_model |
field_definition, validation_rule, calculation, relationship, lifecycle_state |
view |
happy_path, edge_case, error_case |
integration |
inbound_sync, outbound_sync, transform, error_case |
business_logic |
happy_path, edge_case, error_case, rule_branch |
non_functional |
performance_target, availability_target, accessibility_check |
A requirement with no requirement type accepts any of the fifteen values.
All accepted attributes:
| Attribute | Type / values |
|---|---|
title |
string, required |
type |
one of the fifteen values in the table above |
format |
gherkin |
details |
object (see above) |
validation_tag |
visual, data, authz, integration, perf |
priority |
low, medium, high, critical (default medium) |
review_status |
candidate, confirmed, dismissed, flagged (default candidate) |
discovery_method |
manual, scan (default manual) |
confidence |
decimal from 0 to 1 |
actor, trigger, outcome, frequency, category |
string |
note, dismiss_reason |
string, up to 2,000 characters |
requirement_id |
UUID of a requirement in your organization |
application_id |
UUID of an application in your organization |
capability_id |
UUID of a capability in your organization |
application_version_id |
UUID of an application version; leave it out for a use case that spans versions |
Link fields are set as attributes (requirement_id), not through a relationships block.
Response (201, attributes trimmed):
{
"data": {
"type": "UseCase",
"id": "0199a3f4-6d7e-7f80-9a1b-2c3d4e5f6a7b",
"attributes": {
"title": "Sign in with a valid password",
"type": "happy_path",
"format": "gherkin",
"details": {
"given_clause": "a registered user on the sign-in page",
"when_clause": "they submit a valid email and password",
"then_clause": "they land on their dashboard"
},
"validation_tag": "visual",
"priority": "high",
"review_status": "candidate",
"discovery_method": "manual",
"requirement_id": "0199a3e5-2a3b-7c4d-9e5f-6a7b8c9d0e1f"
},
"relationships": {
"requirement": {
"data": { "type": "Requirement", "id": "0199a3e5-2a3b-7c4d-9e5f-6a7b8c9d0e1f" }
}
}
}
}
A failed validation returns 400 with a JSON:API errors array.
Creating Test Cases
POST /api/v1/test-cases creates one test case. It must be linked to a requirement, a use case, or
both.
curl -X POST https://catalio.ai/api/v1/test-cases \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/vnd.api+json" \
-H "Accept: application/vnd.api+json" \
-d '{
"data": {
"type": "TestCase",
"attributes": {
"title": "Valid password signs the user in",
"description": "Submits a known-good email and password from the sign-in page",
"test_type": "e2e",
"expected_result": "The dashboard renders for the signed-in user",
"use_case_id": "0199a3f4-6d7e-7f80-9a1b-2c3d4e5f6a7b"
}
}
}'
| Attribute | Type / values |
|---|---|
title |
string, required |
description |
string, required |
expected_result |
string, required |
test_type |
unit, integration, e2e, manual (default unit) |
test_steps |
array of objects; Catalio doesn’t enforce keys inside each step |
use_case_id |
UUID of a use case in your organization |
requirement_id |
UUID of a requirement in your organization |
source |
manual, code_extraction, ci_report, browser_qa (default manual) |
repository_id |
UUID of a connected repository |
file_path |
string, up to 1,024 characters: where the test lives |
canonical_test_name |
string, up to 1,024 characters: the test’s name as your runner reports it |
visible_test_id |
string, up to 64 characters (e.g. CT-1042). Catalio allocates these; don’t make one up. |
Errors to expect, all returned as 400:
- Neither
use_case_idnorrequirement_idset:Test case must be linked to either a requirement or a use case. - A
use_case_idorrequirement_idfrom another organization:Use case must belong to the same organization as the test case/Requirement must belong to the same organization as the test case. - A duplicate title:
A test case with this title already exists in the organization. Titles must be unique per organization, repository, and file path, so two manual test cases (no repository, no file path) can’t share a title.
Link a test case to its use case or requirement when you create it. PATCH doesn’t accept
use_case_id or requirement_id.
Response (201):
{
"data": {
"type": "TestCase",
"id": "0199a3f8-0a1b-7c2d-8e3f-4a5b6c7d8e9f",
"attributes": {
"source": "manual",
"use_case_id": "0199a3f4-6d7e-7f80-9a1b-2c3d4e5f6a7b",
"requirement_id": null,
"repository_id": null,
"file_path": null,
"canonical_test_name": null,
"visible_test_id": null
}
}
}
Note
title, description, test_type,
expected_result, or test_steps. They are saved; they’re just not returned by the API.
Uploading CI Test Runs
POST /api/v1/test-runs uploads a JUnit XML report from CI. Catalio stores the report, queues it,
and returns 202 straight away; results appear once ingest finishes.
This endpoint is stricter than the rest of the API:
- Use an API credential’s token (client credentials). A token from a signed-in user gets
403. - The credential must be bound to the repository you’re posting for. An unbound credential, or one
bound to a different repository, gets
403. - The repository must be a GitHub repository connected to your Catalio organization.
- If test-report ingest isn’t enabled for your organization, the endpoint returns
404.
The body is multipart/form-data, not JSON:
curl -X POST https://catalio.ai/api/v1/test-runs \
-H "Authorization: Bearer ${TOKEN}" \
-F "repo=acme/storefront" \
-F "external_run_id=${GITHUB_RUN_ID}-unit" \
-F "suite=unit" \
-F "source=github-actions" \
-F "commit_sha=${GITHUB_SHA}" \
-F "branch=${GITHUB_HEAD_REF}" \
-F "pr_number=482" \
-F "external_run_url=https://github.com/acme/storefront/actions/runs/${GITHUB_RUN_ID}" \
-F "app_version=2.14.0" \
-F "report=@tmp/junit/unit.xml"
| Field | Required | Notes |
|---|---|---|
repo |
yes | owner/name of the GitHub repository |
report |
yes | The JUnit XML file. Maximum 50 MB. |
external_run_id |
yes | Your CI system’s run ID, up to 255 characters |
suite |
no | unit, integration, ui, or e2e. A file-style name works too: unit.xml and unit-p2 both become unit. Anything else, or no value, becomes e2e. |
source |
no | github-actions (default), local, playwright-qa, or manual |
commit_sha |
no | Trimmed and cut to 40 characters |
branch |
no | Up to 255 characters |
pr_number |
no | Positive integer; any other value is ignored |
external_run_url |
no | Link back to the CI run, up to 2,048 characters |
app_version |
no | Free-form version string. Catalio stores it as given and never parses it. |
Don’t send organization_id. The organization comes from the credential, and a request that includes
it is rejected with 422.
Response (202):
{
"id": "0199a401-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
"status": "pending"
}
status describes ingest progress, not your test outcome: pending, processing, completed,
partial, or failed. A run where every test failed still ingests to completed.
Retries are safe. A run is identified by repository, source, external_run_id, and suite.
Posting the same report again for the same run returns 202 with the existing run’s id and current
status, and doesn’t ingest the results twice. Posting a different report for a run that already
exists returns 409. Use a new external_run_id (or a different suite) for each report file.
Errors. These come back as {"error": "<message>"}:
| Status | error |
|---|---|
403 |
M2M API credential required |
403 |
API credential is not scoped to a repository |
403 |
API credential is not authorized for this repository |
404 |
Not found (test-report ingest not enabled for the organization) |
404 |
Repository not found (repo missing, malformed, or not connected) |
409 |
A different report was already ingested for this run identity |
413 |
Report exceeds maximum size |
422 |
organization_id must not be supplied |
422 |
report file is required |
422 |
Invalid submission (for example, missing external_run_id or an unrecognized source) |
A missing or invalid token returns 401 in the JSON:API error format shown in
API Authentication.
Application Context (v2)
GET /api/v2/context returns one application’s confirmed features with the requirements under each,
in a single response. It’s meant for agents and tools that want the whole tree without walking
JSON:API relationships.
curl "https://catalio.ai/api/v2/context?application_id=0199a3e2-5b7c-7d10-9f2a-3c4d5e6f7a8b" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/json"
application_id is the only query parameter, and it’s required.
Response (200):
{
"application": {
"id": "0199a3e2-5b7c-7d10-9f2a-3c4d5e6f7a8b",
"name": "Storefront"
},
"features": [
{
"id": "0199a3e3-7c8d-7e9f-8a0b-1c2d3e4f5a6b",
"name": "Checkout",
"description": "Cart review, payment, and order confirmation",
"requirements": [
{
"id": "0199a3e5-2a3b-7c4d-9e5f-6a7b8c9d0e1f",
"title": "Apply a discount code",
"user_want": "enter a discount code at checkout",
"user_benefit": "I pay the promotional price"
}
]
}
]
}
Only features with a confirmed status are included; detected and dismissed features are left out.
Requirements within a feature are sorted by title. This is plain JSON, not JSON:API, so there’s no
data wrapper, filtering, or pagination.
| Status | Body | When |
|---|---|---|
422 |
{"error": "application_id is required"} |
No application_id parameter |
404 |
{"error": "Application not found"} |
The ID doesn’t exist, isn’t a valid UUID, or belongs to another organization |
401 |
JSON:API error (see above) | Missing or invalid token |
500 |
{"error": "Internal server error"} |
Features couldn’t be loaded |
Next Steps
- JSON:API Overview: filtering, sorting, pagination, and the full resource list
- API Authentication: create credentials and get tokens
- Swagger UI: try the JSON:API routes interactively