API Guides 12 min read

API Endpoint Reference

Request and response shapes for change proposals by application, use case and test case creation, CI test-run uploads, and the v2 context endpoint.

Updated
On this page

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.

Bash
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):

Json
{
  "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

Bash
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_id and snapshot_before must be null when action_type is create.
  • target_id is required when action_type is update, and for connect (except when target_type is requirement_relation).
  • confidence_score is between 0.0 and 1.0, and can’t exceed 0.55 when source is meeting_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 returns 201 with 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).

Bash
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):

Json
{
  "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.

Bash
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_id nor requirement_id set: Test case must be linked to either a requirement or a use case.
  • A use_case_id or requirement_id from 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):

Json
{
  "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

Test case responses don’t currently include 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:

Bash
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):

Json
{
  "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.

Bash
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):

Json
{
  "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