Hyperstruck
API

Claims API

Review what your agents believe, decide on the claims held for review, correct identity mistakes, and erase everything known about an entity or clear an agent's corpus entirely.

Use the Claims API to inspect and govern the claims your agents have learned. For what a claim is and why the shelf exists, read Claims first.

Access control

Three scopes gate this surface. claims:read inspects, claims:curate decides, claims:erase deletes. None implies another.

Separation of duty applies to API keys: a single key cannot combine a dispatch scope with a curation scope, so an automated agent cannot approve content it ingested. It does not apply to portal roles, where the developer role holds both agents:write and claims:curate. If you need the reviewer to be a different principal from the dispatcher, issue separate API keys; the role model alone will not enforce it. See Access control (FGA and RBAC).

Start with the queues

The tenant-wide queues below are the entry point. They tell you what is waiting; everything else acts on one item you found there.

List claims held for review

GET /org/claims/quarantine returns the claims waiting on a decision, each stating why it is held. It is tenant-wide, and every item is labeled with the agent that owns it, so one call surveys every agent you run.

curl -H "Authorization: Bearer <API_KEY>" \
  "https://api.hyperstruck.com/org/claims/quarantine"

Prop

Type

Required scope: claims:read

Each item carries claim_id, agent_id, entity_id, canonical_name, statement, provenance_class, channel_trust, tool_family, quarantine_cause, is_release_lapsed, corroboration_count, and recorded_at. Note that corroboration_count here is the distinct-origin count, which is not the same number the review-context endpoint returns under that name. The response also carries filtered and withheld_count.

statement is an object, not a string, and this matters

A held claim's text is attacker-reachable, so statement is a structured RenderedText, not a plain string. Render display_text: it has URLs defanged and hidden characters made visible, and carries no markup, so a console showing it as plain text cannot be made to load a remote image or follow a link. Keep raw_text behind an explicit toggle. findings, visible_char_count and raw_char_count expose padding and deception that display_text alone would not convey, and is_collapsed_by_default tells your console when to hide it. Binding statement directly into a template is exactly the mistake this shape exists to prevent.

A space-scoped key sees a partial queue

These queues are tenant-wide only for a key with full space reach. When the caller's readable-space set is restricted, rows are filtered out and filtered is true, with withheld_count giving a best-effort per-page count of what was omitted. Check filtered before treating an empty page as an empty backlog: the two are indistinguishable from items alone.

quarantine_cause is what decides who may act. A claim held because it arrived through a channel an attacker could reach is a different problem from a consequential change out of an ordinary source, and only the first requires an administrator. Treat an unfamiliar cause as needing escalation rather than as safe: a newer version can introduce one, and defaulting an unknown cause to releasable is the failure this field exists to prevent.

is_release_lapsed is a separate axis from the cause. A previously released claim that lost its release still carries the reason it was originally held, so a lapse can never widen who is allowed to release it.

Nothing lapses today

The mechanism that would revert an unvindicated approval currently runs in observation only: it computes what it would revert and reverts nothing. In practice is_release_lapsed is always false and the lapsed-release path below is unreachable. Both are documented because the field ships and the behavior is designed, but do not build a workflow that depends on approvals expiring on their own. See Claims.

List unfiled claims

GET /org/claims/abstained returns claims stored without a filing slot, waiting for you to say which property they belong to. Same paging and scope as the queue above. Items carry the same identifying fields, without the quarantine ones.

List proposed splits

GET /org/claims/split-proposals returns properties that keep changing value, where the proposal is to split them into qualified variants rather than keep superseding one. Same limit and scope, but this queue pages by when the proposal was created rather than when a claim was recorded. The quarantine and unfiled queues share a sort key, so do not assume a cursor is portable between queues; always follow the next_cursor from the queue you are paging.

Read an entity's dossier

GET /agents/{agent_id}/claims/entities/{entity_id} returns every version of every claim the agent holds about one thing, including the held and disputed versions the agent itself never sees.

curl -H "Authorization: Bearer <API_KEY>" \
  "https://api.hyperstruck.com/agents/<AGENT_ID>/claims/entities/<ENTITY_ID>"

Required scope: claims:read

The response carries entity_id, agent_id, canonical_name, and claims. Each version reports its claim_id (the identifier every mutation on this page takes), its statement (the claim text, as the same RenderedText object described above), status, provenance_class, attribute_key, valid_from and valid_until, plus is_bindable, is_quarantined, quarantine_cause, is_release_lapsed, and is_operator_asserted.

Read is_bindable as the honest answer to can the agent actually use this right now. A claim can be present, uncontested, and still not bindable.

Get a claim's review context

GET /agents/{agent_id}/claims/{claim_id}/review-context returns the provenance, cause, and corroboration a reviewer needs before deciding, plus the consent token every mutation requires.

Three numbers describe the evidence, and reading them correctly starts with knowing that none of them is the independence statistic the bar is actually applied to.

distinct_origins counts distinct attested call frames, cumulatively over the claim's life. It is deduplicated, so re-harvesting the same step does not inflate it, but a distinct call frame is not an independent source: the same tool reading the same poisoned page on a later run arrives on a fresh frame and would otherwise corroborate itself.

corroboration_count counts only the corroboration events still inside the retained window of the most recent 128. It is therefore never larger than distinct_origins, and it is smaller on any claim that has accumulated more than that.

k_required is a fixed bar of 2 for clearing a high-stakes hold, not something that varies per claim. It is measured against a count of distinct provenance roots, meaning the tool family, domain, or ingest channel an observation ultimately came from, which is what expresses genuine independence. Neither displayed number is that count, except on older claims recorded before roots were captured, where the check falls back to distinct_origins. So read these three as a description of the evidence, not as the arithmetic being tested.

curl -i -H "Authorization: Bearer <API_KEY>" \
  "https://api.hyperstruck.com/agents/<AGENT_ID>/claims/<CLAIM_ID>/review-context"

Required scope: claims:read

The ETag is required to act

The response carries an ETag header. Pass it back as If-Match on release, promote, and adopt. It hashes the fields a decision depends on, so if anything about the claim changed after you read it, your token is stale and the mutation is refused with a 412. You cannot approve a version you did not inspect. Treat a 412 as "re-read and decide again", never as a reason to retry the same token.

Expect three failure codes across the mutations on this page: 412 when the consent token is stale or missing, 403 when the claim's cause is not one your path may clear (a curator hitting an untrusted claim, for example), and 409 when the action was already taken.

Resolve a property id

GET /agents/{agent_id}/claims/attributes/{attribute_id} resolves a property's registry id to its key, so you can confirm you are filing an unfiled claim under the slot you meant. Adoption is one-shot, so this exists to be called first. Required scope: claims:read.

Release a held claim

POST /agents/{agent_id}/claims/{claim_id}/release returns a held claim to verification, carrying your attestation.

curl -X POST -H "Authorization: Bearer <API_KEY>" \
  -H 'If-Match: "<ETAG>"' \
  "https://api.hyperstruck.com/agents/<AGENT_ID>/claims/<CLAIM_ID>/release"

Required scope: claims:curate. Requires If-Match.

This path handles the causes a curator may clear: a consequential change from an ordinary source, and a lapsed release. Anything else is refused here on purpose, including a claim held because its channel was attacker-reachable, one whose identity could not be resolved, and one with no recorded cause.

Releasing is an endorsement, not a verification. The claim re-enters verification rather than skipping it: if it clears the corroboration bar on its own evidence it is released outright, and if it cannot, it is readable but on probation and never treated as established. See Claims.

Release as an administrator

POST /agents/{agent_id}/claims/{claim_id}/admin-release releases any held claim, including one that arrived through a channel an attacker could reach.

Required scope: claims:erase, and administrator on the agent. Requires If-Match.

This is the sharpest action on the surface, which is why it sits at the administrator tier: it moves attacker-reachable content into a corpus the planner reads. Even here the ceiling holds. Such a claim reaches only the fenced reference lane, marked as a human assertion, and is permanently refused from the places where a value sits beside a tool's own instructions.

Promote a disputed value

POST /agents/{agent_id}/claims/{claim_id}/promote makes a disputed alternative the binding version for its property.

Prop

Type

Required scope: claims:curate. Requires If-Match. A second promotion of the same claim returns 409.

Choose reason honestly. It is the field that decides whether a source loses standing, and defaulting a genuine correction to world_change is how an unreliable source keeps its reputation.

File an unfiled claim

POST /agents/{agent_id}/claims/{claim_id}/adopt assigns a property to a claim stored without one, so it can take part in supersession.

Prop

Type

Required scope: claims:curate. Requires If-Match. A second adoption returns 409.

Resolve a split proposal

POST /agents/{agent_id}/claims/split-proposals/{proposal_id}/resolve confirms or rejects a proposed split.

Prop

Type

Required scope: claims:curate. Re-resolving an already-decided proposal returns 409.

Correct identity

Identity mistakes are the destructive kind, so every edge here is reversible and none of them is a hard merge.

GET and POST /agents/{agent_id}/claims/entities/{entity_id}/aliases list or author the alternative names an entity is known by, for example ashgrove alongside Ashgrove Freight.

Prop

Type

Reading requires claims:read; authoring requires claims:curate.

The remaining corrections all require claims:curate:

EndpointWhat it does
POST /agents/{agent_id}/claims/aliases/{alias_id}/deactivateToggles an alias off. Reversible, never a hard merge.
GET /agents/{agent_id}/claims/attribute-mergesLists places where two differently phrased property names were folded together.
POST /agents/{agent_id}/claims/attribute-merges/{merge_id}/reverseWithdraws a merge.

Erase an entity

POST /agents/{agent_id}/claims/entities/{entity_id}/erasure deletes an entity together with its claims, aliases, dossier, and split proposals, and returns a durable receipt.

curl -X POST -H "Authorization: Bearer <API_KEY>" \
  "https://api.hyperstruck.com/agents/<AGENT_ID>/claims/entities/<ENTITY_ID>/erasure"

Required scope: claims:erase, and administrator on the agent

Before the delete, the support those claims contributed to your agent's learnings is subtracted back out, so no learning keeps standing that was granted by evidence you just erased. The operation is idempotent: a repeat request returns the original receipt.

This is a claim-layer erasure, not a complete one

Learnings, graph nodes, raw run traces, and usage aggregates are not deleted by this call. The receipt names each one it did not reach, rather than implying a completeness it does not have. The ledger is built so those remaining legs can be replayed against requests served today.

Read the erasure receipts

Every erasure writes a receipt, and both of these read them back without erasing anything.

EndpointWhat it returns
GET /agents/{agent_id}/claims/erasuresEvery erasure performed for this agent, newest first, so "what have we erased" can be answered without knowing each entity in advance. Takes limit and cursor.
GET /agents/{agent_id}/claims/entities/{entity_id}/erasureOne entity's receipt, or 404 if it was never erased.
curl -H "Authorization: Bearer <API_KEY>" \
  "https://api.hyperstruck.com/agents/<AGENT_ID>/claims/erasures"

Required scope: claims:erase, and administrator on the agent. A receipt records what the erasure reached, what it deliberately did not, who asked for it, and any reason and ticket reference you supplied. has_residue is true when the erasure could not reach every run that had been served the entity's facts, which status alone cannot tell you.

Clear everything an agent knows

DELETE /agents/{agent_id}/claims erases every entity the agent holds, and with the last one it drops the property names that are left naming nothing.

curl -X DELETE -H "Authorization: Bearer <API_KEY>" \
  "https://api.hyperstruck.com/agents/<AGENT_ID>/claims"

Required scope: claims:erase, and administrator on the agent

This is the entity erasure above, repeated. Each entity goes through the same withdrawal and leaves its own receipt, so clearing a corpus never becomes a bulk delete that strips the claims out and leaves your agent's learnings standing on evidence that is gone.

One request erases a batch rather than the whole corpus. The response carries remaining_entities, and a number above zero means call again:

{
  "entities_erased": 25,
  "claims_erased": 61,
  "attributes_removed": 0,
  "remaining_entities": 4,
  "has_residue": false,
  "request_ids": ["..."]
}

Calling again is always safe. An entity that is already erased is skipped rather than erased twice, so a request that times out mid-way is resumed by simply repeating it. The dashboard does this repeating for you.

You can record why, and it is written onto every receipt the call produces:

curl -X DELETE -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"reason": "customer erasure request", "ticket_ref": "DSR-2291"}' \
  "https://api.hyperstruck.com/agents/<AGENT_ID>/claims"

A 404 means no such agent, which is deliberately different from an agent that holds nothing: that answers 200 with entities_erased and remaining_entities both zero. A 503 means some entities in the batch were erased and others could not be. The receipts already written stand, nothing is half-erased, and repeating the call picks up where it stopped.

The same claim-layer limit applies, once per entity

Every receipt this writes names what it did not reach, exactly as a single erasure does: learnings, graph nodes, raw run traces, and usage aggregates are not deleted. Read the receipts from GET /agents/{agent_id}/claims/erasures.

Clearing is not a barrier

Nothing stops your agent recording new facts while this runs, so a fact learned mid-clear is counted in remaining_entities and erased on a later pass, or after the last one, not at all. Stop the agent first if the corpus has to stay empty.

Read how warm the corpus is

GET /agents/{agent_id}/claims/stability reports how much of an agent's claim corpus has actually demonstrated that it holds. GET /org/claims/stability gives the same reading for every agent in the tenant, plus a total, so the review can happen without knowing every agent id.

curl -H "Authorization: Bearer <API_KEY>" \
  "https://api.hyperstruck.com/agents/<AGENT_ID>/claims/stability"

Required scope: claims:read. The org view takes limit (1 to 100, default 50) and sets is_truncated when the tenant has more agents than that, in which case the total covers only the agents listed.

This is the readout behind the one claim behavior that is deliberately switched off, deleting a lookup the planner asked for. A remembered value earns that only by having been seen to hold across real elapsed time, which a young corpus has not.

PopulationWhat it counts
bindable_slotsKeyed, open, non-quarantined slots: everything the axis judges at all.
never_re_observed_slotsSlots never seen to hold for any elapsed time. A cold corpus is nearly all of these.
re_observed_slotsSlots with any demonstrated survival, which is the population that can qualify.
admitted_slotsWhat the axis would admit right now.
lapsed_slotsRe-observed slots whose licence no longer covers the gap since they were last seen.
slots_with_a_recorded_changeSlots observed to have moved to a different value at least once.
mean_survived_daystotal_survived_days over re_observed_slots, so numerator and denominator cover the same population.
confidence, prior_changesThe settings these counts were evaluated at.

Read admitted_slots as a series, and as a ceiling

Two things about that number. It is an upper bound on read elimination rather than a count of it, because the bind gate independently refuses a currency-decayed claim and a high-stakes property, neither of which this axis judges. And a near-empty count on a young corpus is the design, not a fault: the axis warms as claims prove themselves. Watching rows move out of never_re_observed_slots into admitted_slots and lapsed_slots is what distinguishes a corpus that is warming from one whose axis never accrues, and a single reading cannot tell you which you have.

A 501 means the deployed Core predates this axis, so there is no readout to return. On such a pin it is the answer to every call and retrying will not help. A 503 means a claim migration has not been applied to the database.

See what it cost

Claim writes are billable, and a claim is billed once, at the moment it first becomes able to help you. A claim that entered unable to be used, such as a correctly held injection attempt, is not billed until it earns its place, so nobody can inflate your bill by feeding your agent poisoned observations.

GET /usage/claim-assists reports how often the corpus actually helped: bound claims proven correct or wrong at execution, and how many runs it assisted. These events never bill. See Usage API.