Endpoint reference
A reference to the Hyrax REST API, grouped by resource. Every endpoint lives under /api/..., takes and returns JSON, and authenticates with an hk_live_ API key (-H "Authorization: Bearer hk_live_...", omitted from the examples below). Start with the API overview for authentication, the error envelope, pagination, and rate limits. This page is the guided tour — what each route is for and what a status code MEANS; for the full request and response schemas of any operation, open the API explorer, which is generated from the live API.
Repositories are addressed by the owner/repo/branch triple in path segments (for example /api/repos/acme/billing-service/main/jobs). Where a repo is named in a query parameter — the repo filter on observations, repo_name on a forecast lookup — use the shorter repo@branch slug (for example billing-service@main). Observations are addressed by their HYRAX-N ref, issues by their ISS-N ref, and missions by their MSN-N ref (or the number N under the repo path).
Jobs
A job is one run of a workflow against a repo.
| Method | Path | Description |
|---|---|---|
POST | /api/repos/{owner}/{repo}/{branch}/jobs | Submit a job for a repo. |
GET | /api/repos/{owner}/{repo}/{branch}/jobs | List a repo's jobs (paginated). |
GET | /api/jobs | List all jobs across the workspace (paginated). |
GET | /api/jobs/{job_id} | Get a single job's detail. |
POST | /api/jobs/{job_id}/cancel | Cancel a pending or running job. REST-only — not exposed as an MCP tool. |
POST | /api/jobs/{job_id}/retry | Retry a failed or cancelled job, preserving its workflow. Returns 201 with the new retry job, or 200 when an in-flight retry of the same parent already exists (idempotent match — the returned job is the existing retry; no new work was enqueued). |
Submit a job
The body is { "workflow": "...", "params": { ... } }. workflow is one of standard_audit, audit, discover, revalidate, review, or fix — all available on every plan; see Workflows. Retired workflows — benchmark, improve, learn, meta_review, mini_audit, publish, scan and suggest — return 422 with a message explaining the retirement. params is the per-workflow parameter object (often {}).
curl -s -X POST \
"https://api.hyrax.dev/api/repos/acme/billing-service/main/jobs" \
-H "Content-Type: application/json" \
-d '{ "workflow": "standard_audit", "params": {} }'
The response is a JobResponse. The status starts at pending and moves through running to one of completed / failed / cancelled:
{
"id": "...",
"workflow": "audit",
"status": "pending",
"repo_name": "billing-service@main"
}
A fresh submission returns 201. When the submission matches an in-flight or recently-completed job with identical parameters on the same commit — or matches an Idempotency-Key header you sent within the last 24 hours — the endpoint returns 200 with the existing job instead (no new work was enqueued). Both codes carry the same JobResponse shape, so treat them as semantically distinct: only a 201 means your request started a run. Send an Idempotency-Key header to make network retries safe, and pass ?force=true to push a fresh submission past the short soft-dedupe window on terminal jobs.
A request that exceeds your credit or a budget comes back as 402 billing_error. The same envelope also covers a workflow your workspace isn't entitled to — error.details.allowed_workflows then lists the workflow entitlements it does hold — though every workflow named above is on both plans, so that arm isn't reachable on a live plan today. If you do read that list, retry with one of the verbs named above rather than any name in it: it's derived from your workspace's entitlements, so it can also carry an internal verb that no longer accepts submissions (that one comes back 422). A private-beta workflow that isn't enabled for your workspace comes back as 403 permission_error with code: "feature_not_enabled". See Errors.
Repositories
Manage the repos in your workspace and read their discovery output.
| Method | Path | Description |
|---|---|---|
GET | /api/repos | List repos in the workspace (paginated). |
POST | /api/repos | Register a new repo. |
GET | /api/repos/{owner}/{repo}/{branch} | Get a repo's detail. |
DELETE | /api/repos/{owner}/{repo}/{branch} | Remove a repo from the workspace (204). Requires the delete_repo permission (an admin action by default). Soft-delete only. REST-only — not exposed as an MCP tool. |
POST | /api/repos/{owner}/{repo}/{branch}/reset | Reset a repo to a clean state. Body flags pick what to clear: { "findings": ..., "history": ..., "suggestions": ..., "wipe": ... } (suggestions matters only to a workspace that has audit suggestions turned on). REST-only — not exposed as an MCP tool. |
GET | /api/repos/{owner}/{repo}/{branch}/discovery | Discovery files for the repo. |
GET | /api/repos/{owner}/{repo}/{branch}/knowledge | The repo's knowledge bundle. |
GET | /api/repos/{owner}/{repo}/{branch}/fix-prs | Fix PRs opened for the repo. |
Register a repo
POST /api/repos returns 201. Set access_mode to "installed" (the default — Hyrax clones through your GitHub App installation) or "anonymous" for a public repo you haven't installed on.
Registering a repo usually enqueues a discover job, installed or anonymous — but not always, so don't build a client that waits for a run on every registration. No job is created when:
- the repo already has a completed discovery. Re-registering a repo you removed can bring its earlier discovery back with it. Submit
discoveryourself when you want a fresh one. - your workspace's usage limits refuse the job — for example no credit left, spend paused, too many jobs already queued or running, or a per-job spend cap. This is silent: the registration still returns
201, and nothing in the response says the job was refused. - automatic discovery is paused for every workspace or for yours. This is silent too.
Registering a repo through the MCP server or chat never enqueues one.
The response doesn't return the job's id. The enqueue runs on a separate thread, so its job id isn't knowable when the response is built: discover_job_id on the 201 is always null and discovery_queue_failed is always false — don't build a poll or a check around either field. To find the run, list the repo's jobs a few seconds later: GET /api/repos/{owner}/{repo}/{branch}/jobs?workflow=discover. The row appears within a second or two but doesn't start until the next dispatch tick (~30s), so give it that long before concluding nothing happened. If it genuinely never appears on a registration that should have fired one, you can submit discover yourself — but note the auto-enqueue goes through the same admission gates a manual submission does, so if it was refused for billing reasons (exhausted plan credit, paused spend) the manual call will be refused identically. A manual retry only helps the transient cases.
No plan limits how many repos you register or how large they are. These refusals are worth handling explicitly:
400 invalid_request_errorwithcode: "repo_no_longer_public"— ananonymousregistration whose repo GitHub won't serve publicly (private, renamed, or gone).409 conflictwithcode: "repo_already_registered"— thatowner/repo/branchtriple is already in the workspace. The body echoes the existing repo's name.503 server_error— Hyrax couldn't verify the repo. Retry; this one may arrive without aRetry-After.
Observations
Observations are what an audit produces: mostly findings, plus advisories, and suggestions for a workspace where Hyrax support has turned audit suggestions on. Each has a stable HYRAX-N ref, which is how you address one. An observation is evidence, not a work item: to track work on one, file it as an issue. See Findings & Code health for the model and lifecycle.
| Method | Path | Description |
|---|---|---|
GET | /api/observations | List observations (filterable, paginated). |
GET | /api/observations/{ref} | Get one observation by HYRAX-N ref. |
POST | /api/observations/{ref}/fix | Spawn a fix job for the observation. |
POST | /api/observations/{ref}/dismiss | Dismiss the observation. |
POST | /api/observations/{ref}/reopen | Reopen a closed observation. |
POST | /api/observations/{ref}/complete | Mark the observation addressed (resolved outside a Hyrax PR) — closes it as completed. Body is optional: { "url": ..., "note": ... }, a proof link and a free-text annotation. |
POST | /api/observations/{ref}/acknowledge | Acknowledge the observation — stamps acknowledged_at; it stays open. No body. |
POST | /api/observations/{ref}/publish-ticket | Publish the observation to the repo's configured ticket tracker. 422 when the repo has no ticket integration. |
List observations
The list endpoint accepts these filters as query parameters:
| Filter | Values |
|---|---|
kind | finding, suggestion, or advisory |
status | new or closed |
priority | P0, P1, P2, P3 |
category | security, correctness, maintainability, performance, architecture, operations |
repo | A repo slug in repo@branch form (e.g. billing-service@main) to scope to one repo |
since | An ISO-8601 timestamp; returns observations updated after it |
close_reason | fixed, dismissed, completed, or expired. Only closed observations carry one, so setting it narrows to closed rows |
has_fix_pr | true keeps only observations Hyrax ever opened a fix PR for; false keeps only the rest. See the note under Fix an observation before combining it with status=new |
easy_wins | true returns only Easy Wins — open findings that are high-priority, cheap, and low-risk. Pair with kind=finding&status=new |
needs_review | true returns only findings that exhausted Hyrax's automatic fix attempts and now want a human. Pair with kind=finding&status=new |
domain | security, code_quality, reliability, data_apis, testing, devops, or ux — narrows to observations found by the audit tools that belong to one finding domain. A different axis from category, not a coarser one |
order | recency (default, newest first), display_rank for the same "top findings" order the app leads with, or resolved to order by when each was resolved (newest first) |
Add include=locations to embed each observation's file locations inline.
curl -s -G "https://api.hyrax.dev/api/observations" \
-d kind=finding \
-d status=new \
-d priority=P0 \
-d category=security \
-d repo=billing-service@main \
-d include=locations
Each observation carries its ref, kind, status, priority, category, title, and description; with include=locations, a locations array of { file_path, symbol, line_start, line_end }. List responses also carry next_cursor and has_more for pagination.
Fix an observation
Spawning a fix job does not close the observation. It moves to closed[fixed] only when the resulting pull request is merged.
One thing to know if you poll: once the fix attempt has a pull request, the observation drops out of GET /api/observations?status=new even though its status is still new — handed-off findings leave the active worklist and surface on {owner}/{repo}/{branch}/fix-prs instead. Watch that lane rather than treating the disappearance as a state change. Adding has_fix_pr=true won't bring it back: the hand-off exclusion still applies to a status=new query, so the two filters cancel out on exactly the rows you're looking for. has_fix_pr is for querying without status=new — it partitions closed observations into the ones Hyrax opened a PR for and the ones resolved another way. If the PR is closed unmerged, the observation reappears in the status=new list on its own.
curl -s -X POST "https://api.hyrax.dev/api/observations/HYRAX-42/fix"
The response carries the updated observation plus the ids of the spawned fix job and its attempt record:
{
"observation": { "ref": "HYRAX-42", "status": "new" },
"attempt_id": "...",
"job_id": "..."
}
Dismiss an observation
The body requires dismissal_category (a structured reason) and dismissal_note (free-text rationale, 10–500 characters, required). The category determines whether a later audit can resurface the observation:
- Stays closed on re-discovery:
false_positive,wont_fix,out_of_scope. - Can reappear if re-found:
stale,no_longer_found, and the catch-allother. (exhausted_fix_attemptsbehaves the same way but Hyrax sets it automatically — you won't normally submit it.)
curl -s -X POST \
"https://api.hyrax.dev/api/observations/HYRAX-42/dismiss" \
-H "Content-Type: application/json" \
-d '{
"dismissal_category": "wont_fix",
"dismissal_note": "Accepted risk for v1; tracked in our backlog."
}'
Reopen an observation
reopen_reason (free text, 1–2000 characters) is required.
curl -s -X POST \
"https://api.hyrax.dev/api/observations/HYRAX-42/reopen" \
-H "Content-Type: application/json" \
-d '{ "reopen_reason": "Regressed in the latest release." }'
Forecasts
Forecasts give you an estimated cost range for a workflow on a repo — the same number the app shows before you submit.
| Method | Path | Description |
|---|---|---|
GET | /api/repos/{owner}/{repo}/{branch}/forecasts | Cost forecasts for a repo across workflows. |
GET | /api/forecasts/lookup?workflow=&repo_name= | A single forecast for one workflow on one repo. |
workflow is required; repo_name (the repo@branch slug) is optional — omit it for a workspace-wide estimate.
curl -s -G "https://api.hyrax.dev/api/forecasts/lookup" \
-d workflow=audit \
-d repo_name=billing-service@main
The response is a cost band in USD (cost_low / cost_mid / cost_high) plus a wall-clock duration band in seconds (duration_low / duration_mid / duration_high), with a sample_size and a banded confidence. Use it to check a run fits your remaining credit before you submit.
Team context
Team context entries are key-addressed notes your team and your agents write for later agents.
Every route comes in two forms. The /api/context/entries form addresses the workspace, and the /api/projects/{slug}/context/entries form addresses one Area, Space or project by its slug. Reads return the effective entries at that level, including entries inherited from the workspace and from the levels above. Writes act only on the entries that live at the level you address.
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /api/context/entries/api/projects/{slug}/context/entries | view_context | List the effective entries (summaries, no body; paginated). |
GET | /api/context/entries/digest/api/projects/{slug}/context/entries/digest | view_context | The digest: summaries in reading order, for an agent to read before it starts. |
GET | /api/repos/{owner}/{repo}/{branch}/context-digest | view_context | The digest for the most specific project, Space or Area that contains the repo, or the workspace's when none does. |
GET | /api/context/entries/{key}/api/projects/{slug}/context/entries/{key} | view_context | One entry in full, body_md and data included. It may be inherited. |
GET | /api/context/entries/{key}/history/api/projects/{slug}/context/entries/{key}/history | view_context | The entry's change history, newest first (paginated). |
POST | /api/context/entries/api/projects/{slug}/context/entries | manage_context | Create an entry. Returns 201. |
PUT | /api/context/entries/{key}/api/projects/{slug}/context/entries/{key} | manage_context | Create or replace the entry with this key. See Conditional requests. |
PATCH | /api/context/entries/{key}/api/projects/{slug}/context/entries/{key} | manage_context | Change some fields. Omitted fields are kept, and data is a JSON merge patch (a null value removes that key). |
DELETE | /api/context/entries/{key}/api/projects/{slug}/context/entries/{key} | manage_context | Disable the entry. The body is { "reason": "..." } and is required. There is no hard delete. |
POST | /api/context/entries/{key}/restore/api/projects/{slug}/context/entries/{key}/restore | manage_context | Restore the most recently disabled entry with this key. No body. |
POST | /api/context/entries/{key}/corroborate/api/projects/{slug}/context/entries/{key}/corroborate | corroborate_context and view_context | Record that your key checked a proposed entry another key wrote. API keys only: a signed-in browser session gets 401. |
Every write and corroboration also needs access to the level: a role on it or on a level above it, and manage_projects for the workspace or for a subtree entry on an Area or a Space. See Who can do what. Confirming an entry is done in the web app by a signed-in person. An API key can't confirm, so no confirm route accepts one.
Anything written with an API key lands as proposed until a person confirms it. To label who is writing, send agent_label in a create, PUT or PATCH body (up to 200 characters, shown on the entry and not verified). Omit it and the entry is labelled with your key's name and prefix.
Create an entry
key, kind, title and summary are required. body_md, data, tags and scope are optional. scope is project or subtree; it defaults to project at a project, and a workspace entry accepts only subtree.
curl -s -X POST \
"https://api.hyrax.dev/api/projects/payments/context/entries" \
-H "Content-Type: application/json" \
-d '{
"key": "release-freeze",
"kind": "warning",
"title": "No deploys during the quarter-end freeze",
"summary": "Do not merge to main between the 28th and the 2nd.",
"body_md": "Finance closes the books then. See [[deploy-runbook]].",
"tags": ["release"],
"scope": "subtree"
}'
The response is the entry, with its ETag in the ETag header and in the etag field. A caller without view_context gets only the entry's id, key, version, etag and status, here and on every other write. A key already held by a live entry at the same level is refused with 409 conflict (code: "key_taken"). The same key at another level is a different entry, and the nearer one wins on read.
List entries
| Filter | Values |
|---|---|
kind | decision, convention, runbook, warning, note or flag |
status | proposed or live |
tag | One tag, matched exactly |
q | Full-text search over title, summary and body, in web-search syntax (quoted phrases, or, -word) |
include_disabled | true also lists this level's own disabled entries. Inherited disabled entries never appear |
Filters apply to the entries that win at this level. They never surface an entry that a nearer one overrides. limit defaults to 50 and is at most 200. The response carries head_etag, a fingerprint of the whole effective set, and proposed_count, the number of this level's own proposed entries. The digest takes limit too: 40 by default, at most 100. Its truncated field says whether more entries apply than it returned.
Conditional requests
Every entry response carries ETag: "<entry id>:<version>".
PATCH,DELETEandcorroboraterequireIf-Matchwith the entry's current ETag. Without it they return428(code: "precondition_required").PUTtakes exactly one of two headers.If-None-Match: *creates the entry and returns201, or409 key_takenif a live entry holds the key.If-Matchreplaces the existing entry:title,summary,kindandbody_mdare replaced,datais replaced when you send it, andtagsandscopeare kept when you omit them. Sending both headers is a400, and sending neither is a428.- A stale or mismatched
If-Matchreturns409 conflictwithcode: "version_conflict". Itsdetails.current_etagis the entry's current ETag: re-read it, re-apply your change, and retry. When you also holdview_context,details.currentcarries the current entry. AnIf-Matchthat isn't an entry ETag at all is a400withcode: "malformed_etag". - The list and digest routes return an
ETagfor the whole set and answer304 Not Modifiedto a matchingIf-None-Match, so a client can poll cheaply.
Errors
The main codes these routes add are below. Branch on code; see Errors for the envelope.
| HTTP | code | When |
|---|---|---|
403 | project_access_required | You have no role on this level or on a level above it. |
403 | self_corroboration | Your key created or last changed this entry, so a different key has to corroborate it. |
404 | project_not_found | The project doesn't exist, or your repository access doesn't cover it. Both answer the same way. |
404 | not_found | No live entry holds the key at this level, or, on restore, no disabled entry does. |
409 | key_taken | A live entry at this level already holds the key, on create or on restore. |
409 | version_conflict | The entry changed since the ETag you sent. |
409 | not_proposed, already_corroborated | Only a proposed entry can be corroborated, and by one key at a time: once a key has corroborated it, another key can't until the entry next changes. |
422 | field_too_large, key_too_long | A field, or one tag, is over its size limit. details names the field, the limit, the actual size and the unit. |
422 | secret_detected | A field looks like it carries a credential. The value is never echoed. |
422 | nul_byte | A path, query or body value contains a NUL (U+0000) character. |
422 | lone_surrogate | A path, query or body value contains an unpaired surrogate (U+D800 to U+DFFF), such as a lone \ud800 escape. |
422 | key_reserved | The key organization-context holds the workspace's organization context, so these routes refuse to create, change, disable or restore it at any level. See Organization context. |
422 | key_not_addressable, key_padded, scope_invalid, tags_invalid, blank_field | The key, scope, tags or a required field isn't valid: for example a workspace entry with scope: "project", or more than 20 tags, a blank tag or a duplicate. The message says which rule. |
428 | precondition_required | A required If-Match or If-None-Match header is missing. |
A body that fails schema validation, such as an unknown kind or a missing required field, is 422 validation_error with code: "invalid_payload".
A request without a permission it needs, whether the route's own or manage_projects where a write needs it, gets 403 permission_error. The limits behind these refusals are on the Team context page.
Projects, issues and missions
Projects and issues and missions aren't covered by this reference yet. AI tools reach them as tools on the MCP server, and the web app covers the rest. Missions are in beta: Hyrax support turns them on per workspace, on the Paid plan.
See also
- API overview — authentication, errors, pagination, rate limits.
- Findings & Code health — the observation model and lifecycle.
- Workflows — what each workflow does.
- Issues and Missions — the work model, reached over the MCP server.