Skip to main content

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.

MethodPathDescription
POST/api/repos/{owner}/{repo}/{branch}/jobsSubmit a job for a repo.
GET/api/repos/{owner}/{repo}/{branch}/jobsList a repo's jobs (paginated).
GET/api/jobsList all jobs across the workspace (paginated).
GET/api/jobs/{job_id}Get a single job's detail.
POST/api/jobs/{job_id}/cancelCancel a pending or running job. REST-only — not exposed as an MCP tool.
POST/api/jobs/{job_id}/retryRetry 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.

MethodPathDescription
GET/api/reposList repos in the workspace (paginated).
POST/api/reposRegister 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}/resetReset 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}/discoveryDiscovery files for the repo.
GET/api/repos/{owner}/{repo}/{branch}/knowledgeThe repo's knowledge bundle.
GET/api/repos/{owner}/{repo}/{branch}/fix-prsFix 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 discover yourself 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_error with code: "repo_no_longer_public" — an anonymous registration whose repo GitHub won't serve publicly (private, renamed, or gone).
  • 409 conflict with code: "repo_already_registered" — that owner/repo/branch triple 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 a Retry-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.

MethodPathDescription
GET/api/observationsList observations (filterable, paginated).
GET/api/observations/{ref}Get one observation by HYRAX-N ref.
POST/api/observations/{ref}/fixSpawn a fix job for the observation.
POST/api/observations/{ref}/dismissDismiss the observation.
POST/api/observations/{ref}/reopenReopen a closed observation.
POST/api/observations/{ref}/completeMark 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}/acknowledgeAcknowledge the observation — stamps acknowledged_at; it stays open. No body.
POST/api/observations/{ref}/publish-ticketPublish 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:

FilterValues
kindfinding, suggestion, or advisory
statusnew or closed
priorityP0, P1, P2, P3
categorysecurity, correctness, maintainability, performance, architecture, operations
repoA repo slug in repo@branch form (e.g. billing-service@main) to scope to one repo
sinceAn ISO-8601 timestamp; returns observations updated after it
close_reasonfixed, dismissed, completed, or expired. Only closed observations carry one, so setting it narrows to closed rows
has_fix_prtrue 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_winstrue returns only Easy Wins — open findings that are high-priority, cheap, and low-risk. Pair with kind=finding&status=new
needs_reviewtrue returns only findings that exhausted Hyrax's automatic fix attempts and now want a human. Pair with kind=finding&status=new
domainsecurity, 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
orderrecency (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-all other. (exhausted_fix_attempts behaves 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.

MethodPathDescription
GET/api/repos/{owner}/{repo}/{branch}/forecastsCost 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.

MethodPathPermissionDescription
GET/api/context/entries
/api/projects/{slug}/context/entries
view_contextList the effective entries (summaries, no body; paginated).
GET/api/context/entries/digest
/api/projects/{slug}/context/entries/digest
view_contextThe digest: summaries in reading order, for an agent to read before it starts.
GET/api/repos/{owner}/{repo}/{branch}/context-digestview_contextThe 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_contextOne 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_contextThe entry's change history, newest first (paginated).
POST/api/context/entries
/api/projects/{slug}/context/entries
manage_contextCreate an entry. Returns 201.
PUT/api/context/entries/{key}
/api/projects/{slug}/context/entries/{key}
manage_contextCreate or replace the entry with this key. See Conditional requests.
PATCH/api/context/entries/{key}
/api/projects/{slug}/context/entries/{key}
manage_contextChange 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_contextDisable 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_contextRestore 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_contextRecord 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​

FilterValues
kinddecision, convention, runbook, warning, note or flag
statusproposed or live
tagOne tag, matched exactly
qFull-text search over title, summary and body, in web-search syntax (quoted phrases, or, -word)
include_disabledtrue 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, DELETE and corroborate require If-Match with the entry's current ETag. Without it they return 428 (code: "precondition_required").
  • PUT takes exactly one of two headers. If-None-Match: * creates the entry and returns 201, or 409 key_taken if a live entry holds the key. If-Match replaces the existing entry: title, summary, kind and body_md are replaced, data is replaced when you send it, and tags and scope are kept when you omit them. Sending both headers is a 400, and sending neither is a 428.
  • A stale or mismatched If-Match returns 409 conflict with code: "version_conflict". Its details.current_etag is the entry's current ETag: re-read it, re-apply your change, and retry. When you also hold view_context, details.current carries the current entry. An If-Match that isn't an entry ETag at all is a 400 with code: "malformed_etag".
  • The list and digest routes return an ETag for the whole set and answer 304 Not Modified to a matching If-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.

HTTPcodeWhen
403project_access_requiredYou have no role on this level or on a level above it.
403self_corroborationYour key created or last changed this entry, so a different key has to corroborate it.
404project_not_foundThe project doesn't exist, or your repository access doesn't cover it. Both answer the same way.
404not_foundNo live entry holds the key at this level, or, on restore, no disabled entry does.
409key_takenA live entry at this level already holds the key, on create or on restore.
409version_conflictThe entry changed since the ETag you sent.
409not_proposed, already_corroboratedOnly 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.
422field_too_large, key_too_longA field, or one tag, is over its size limit. details names the field, the limit, the actual size and the unit.
422secret_detectedA field looks like it carries a credential. The value is never echoed.
422nul_byteA path, query or body value contains a NUL (U+0000) character.
422lone_surrogateA path, query or body value contains an unpaired surrogate (U+D800 to U+DFFF), such as a lone \ud800 escape.
422key_reservedThe 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.
422key_not_addressable, key_padded, scope_invalid, tags_invalid, blank_fieldThe 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.
428precondition_requiredA 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​