Hyperstruck
API reference

Claims API

List an agent's claims, read everything about one entity or what changed since a date, settle conflicts, correct claims, fix duplicate entities, and erase claims.

In short

These routes read and change one agent's claims. Reads give you one item per line, with the claim that holds now, its state and its sources. Writes record a decision by a person or an API key, and a retry never writes twice.

Before you start

Every route is under https://api.hyperstruck.com/agents/{agent_id}, where agent_id is the agent's UUID, and takes Authorization: Bearer <API_KEY>. Each needs one of these scopes:

ScopeWhat it allows
claims:readEvery read on this page, and Claims search
claims:correctSettling a conflict, correcting or adding a claim, and marking an attribute protected
claims:curateEverything claims:correct allows; for a signed-in person, also fixing who is who and changing attributes
claims:eraseErasing an entity and clearing an agent's claims; also needs administer access to the agent

claims:correct and claims:curate are never passed on to a run your agent starts. Access control covers how scopes are granted.

A write also needs curate access to the agent. Contributors and stewards of its home space have it, and so do its managers, org admins and write keys that reach its home space. A write to a claim also needs the right to write into the claim's space.

Some decisions are for a person, not an API key: merging or splitting entities, adding a name, folding attributes together, changing an attribute's nature, and removing protection. An API key that tries gets 403 person_required.

Protected values are withheld

Every read on this page hides the values of protected attributes, such as a bank account number, wherever they appear: in a claim's value and statement, in the text written for your model, and in document passages. A value with four or more digits reads by its last four, Ends 5560; a shorter one reads Withheld. Ids and cursors are never changed.

The response to a write is not a read, and shows the values in full.

To see the values on a read, add reveal=true to the query. Only a caller holding claims:curate who may also change the agent may ask. Anyone else gets 403 curate_required.

Read claims

To ask a question in words, such as "Who looks after the Kestrel Foods account?", use Claims search. The routes below read claims by entity, by date or by state.

List claims

GET /agents/{agent_id}/claims lists the agent's claims, one item per line. With no parameters you get every line, a page at a time; the parameters narrow it.

ParameterMeaning
entityOnly this entity's lines. An entity that was merged into another reads as the one it merged into
scopeThis entity and every entity one standing relation away from it, in either direction
statesettled, unanswered, conflicting, open, done or ended (what they mean)
kindvalue, reading, obligation, event, view or relation
attributeOnly lines of these attributes; repeat it for several
ownerObligations this entity is recorded as owing
due_before, due_afterObligations due before this time, or at or after it
duedated for obligations with a due date, none for those without
documentOnly lines with a claim that cites this document (the id a claim's document carries)
containerOnly lines citing a document from this thread or channel
raised_after, raised_beforeOnly lines whose current claim was recorded at or after, or before, this time
orderkey (the default), raised for newest first, or due for soonest due first. due needs kind=obligation; due_before and due_after imply it
as_ofThe claims as they stood at that time
detailHow much each line carries: concise (the default), cited or full (below)
limit1 to 200; default 50
cursorThe previous page's cursor, passed back unchanged

Times need a time zone offset (2026-11-01T00:00:00Z); a time without one is a 422. An entity or owner that is not held, or that you cannot read, is a 404. An entity with no claims gives "items": [].

The response carries items, total (how many lines the filter matches across every page) and cursor, which is null on the last page. A read of dated obligations also carries overdue, how many of them are past due. A cursor works only with the order and filters it was handed out under; with any other it is 422 invalid_cursor.

curl -G "https://api.hyperstruck.com/agents/<AGENT_ID>/claims" \
  -H "Authorization: Bearer <API_KEY>" \
  --data-urlencode "kind=obligation" \
  --data-urlencode "state=open" \
  --data-urlencode "due_before=2026-11-01T00:00:00Z"

Every field of an item is described in The claim item.

Find or list entities

GET /agents/{agent_id}/entities?q=okoye finds the entities whose name or other names match q (1 to 200 characters), best match first. limit is 1 to 50, default 10.

{
  "items": [
    { "id": "22222222-2222-4222-8222-222222222222", "name": "Dr Imani Okoye", "type": "person", "category": "person", "affiliation": null }
  ]
}

type is one of person, organisation, location, project, asset, register_item and agreement. category is the word your documents use for it, such as "patient" or "shipment". affiliation is the entity's newest relation, which helps tell two people with the same name apart, or null.

Leave out q and the route lists every entity the agent holds a live claim about: people, organizations, places, projects and records first, then every other type, each group with the most recently changed first. Narrow it with type, repeated for several. Each entity then also carries details (its live claims that are not obligations), owed (its open obligations), overdue (how many of those are past due) and latest (the attribute and date of its newest change). The response adds total for the type you asked for, type_counts for every type, and a cursor. A cursor from a different type filter is 422 invalid_cursor.

Read an entity's dossier

GET /agents/{agent_id}/entities/{entity_id}/dossier returns everything held about one entity.

FieldMeaning
entityThe entity
claimsIts lines, a page at a time (limit 1 to 200, default 50, and cursor)
aliasesThe other names it answers to
fieldsEach attribute it holds, with how many lines it holds under it
relationsIts links to other entities, the newest 25
owedOpen obligations filed under it, grouped by who owes them, then by due date, the newest 25
possible_duplicatesOpen questions whether it is the same as another entity, each naming both, the rule that raised it and a reason_sentence in plain words
moretruncated is true when relations or owed was cut, and hint says where to list the rest
cursorThe next page of claims, or null on the last

aliases, fields, relations, owed and possible_duplicates come on the first page only.

To see more than 25 obligations, list them with GET /claims?entity=<entity_id>&kind=obligation.

detail, here and in Claims search, sets how much each line carries:

detailEach line carries
concise (default)The current claim; on a conflicting line, the claims competing with it; a run of readings summarized in series
citedEverything in concise, plus every other live claim, the other passages that said each claim (also_stated_by), the words of each run step it came from, and every reading
fullEverything in cited, plus every earlier claim in history

Read what changed

GET /agents/{agent_id}/entities/{entity_id}/changes?since=2026-10-01T00:00:00Z lists every detail of the entity that changed between since and until (default: now), newest first. Each item gives the line, the claim it held before (from) and the claim it holds after (to). from is null for a new detail, and to is null for one that ended.

The response also carries text, the changes written out for your model:

2 changes on Dr Imani Okoye since 1 October 2026.
Clinic days changed on 3 November 2026. Before: From next week Dr Imani Okoye runs her clinics on Tuesday and Thursday mornings. Now: From 3 November Dr Okoye's clinics move to Wednesdays.
Task is new on 20 October 2026: Dr Imani Okoye is to send the referral letter to the patient by 30 October 2026.

Page with limit (1 to 200, default 50) and cursor, sending the same since and until with every page. A cursor from another read, or an until earlier than since, is a 422.

See where an entity's claims came from

GET /agents/{agent_id}/entities/{entity_id}/sources lists the documents that gave claims about the entity, newest first. The documents of one thread, such as a chat channel or an email thread, count as one source.

FieldMeaning
keyNames the source; pass it back as key to read that one alone
channel, container, container_nameThe route the documents came by, and the thread they belong to
document_idThe document, when the source is a single one
title, author, kindThe source's title, author and kind
documents, claimsHow many documents the source covers, and how many claims they gave
first_at, latest_atThe dates of its first and latest documents
latest_doc_idYour own id for its newest document, to open it with the Documents API

The response carries entries, total (sources), total_documents and cursor. limit is 1 to 200, default 50. A cursor from another entity is 422 invalid_cursor.

List what is left to decide

GET /agents/{agent_id}/to-decide lists everything waiting for a person in one place: conflicting lines, and pairs of entities that may be the same. Each entry has a kind: conflict, carrying the line's item in conflict, or duplicate, carrying both entities, the rule that raised it and a reason_sentence in duplicate.

ParameterMeaning
kindconflict or duplicate
entityOnly this entity's own decisions
scopeThis entity and every entity one standing relation away from it
raised_afterOnly decisions raised at or after this time
ordernewest (the default) or oldest
groupnone (the default), document, entity or field; groups then counts each group across the whole filter
limit, cursor1 to 200, default 50

The response carries entries, groups, total and cursor. A pair whose two sides have since been merged is left out.

List the documents that changed something

GET /agents/{agent_id}/claim-documents lists the documents that changed the agent's claims, newest first by the document's own date. A document that changed nothing is not listed. Each entry counts what it did, with the first three lines of each at full detail:

FieldCounts the lines where
establishedIts claim is the current one
endedIt ended or replaced an earlier claim
undecidedOne side of a conflict cites it
since_replacedA later document replaced the value it gave
since_followedA later document gave a different value without announcing a change
since_endedThe value it gave has since ended with nothing in its place

Narrow it with scope (an entity and its neighbors), raised_after or document (one document, by its id or your own doc_id). limit is 1 to 200, default 50.

List the claims a learning uses

A learning can mention a detail that comes from a claim, such as a customer's plan, so it stays right when the detail changes. GET /agents/{agent_id}/learnings/{learning_id}/claims lists each such detail, with the claim it first came from and what it shows now.

FieldMeaning
tokenWhere the detail sits in the learning
distilled_fromclaim_id and state of the claim it first came from: in_force, ended, or gone when that claim no longer exists
resolves_to.stateThe state of the claim that holds the detail now: settled, unanswered, conflicting, open, done or ended; erased when the entity was erased; unresolved when no claim holds it
resolves_to.valueThe value your agent would see, or null when none can be shown
resolves_to.is_value_withheldtrue when a claim holds the detail but its value is in conflict or from a source that cannot be trusted
resolves_to.is_held_for_safetytrue when a safety learning keeps showing the earlier value until a person accepts the change
last_literalThe value the learning shows when the detail has ended or cannot be found

status keeps only the details whose claim is in that state. limit is 1 to 100, default 25, and the next page's token is next_cursor. When the claims cannot be read, the list still answers 200, with is_degraded: true and every detail unresolved; read it again. A cursor that is not this list's is 400 cursor_invalid.

The claim item

Every read returns the same item, one per line.

FieldMeaning
idThe line: <entity id>|<attribute id>|<qualifiers>
entityid, name, type and category
attributeid, name, kind, nature and protected
qualifiersWhat narrows the line, such as {"room": "B"}, or {}
statesettled, unanswered, conflicting, open, done or ended (what they mean)
conflict_kindWhy a conflicting line waits (the reasons), or null
reason_sentenceThe same reason in plain words, or null
conflicts_withFor a conflict between two lines, the other line's id, entity and attribute; otherwise null
currentThe claim that holds now, or null when nothing does
besideOther live claims on the line: the ones competing on a conflicting line, and at cited or full detail every other live claim
historyEarlier claims, newest first, at full detail only
seriesFor a line of readings at concise detail: the count, from and to of the readings summarized behind current
sourcesEach document_id with the passage (a document) or step (a run) the claims come from, once each
word_share, cosineHow well the claim matches a question; filled only by Claims search

Each claim in current, beside and history carries:

FieldMeaning
idThe claim's id, which corrections and verdicts take
valuetype plus the fields that type uses (below)
statementThe source's own words, including any uncertainty it expressed. A correction sent with a value reads as the attribute and the value instead ("Clinic days: Wednesday mornings")
valid_from, valid_untilWhen it holds from, and when it ended; valid_until is null while it holds
recorded_atWhen Hyperstruck recorded it
citeWhere it was said: {"passage": {...}} for a document, {"step": {...}} for a run
excerptA run step's words, at cited and full detail; always null for a document
documentThe source's id, kind, date, author and title, the channel and container_name it came by, and the zone its times were read in
labelFor a claim under the current one, the words your model reads before it, such as "Also on record, 30 August 2026" (every label); null on the current claim
supportHow many passages said it, the first included
also_stated_byThe other passages that said it; empty at concise detail
holder, due_atFor an obligation: the entity it is owed by (id, name, type, category) and when it is due
relatedFor a relation: the other entity

A document's cite is exactly what POST /documents/citations:resolve takes, so you can turn it back into the passage's words. When the document has no version that can be cited, cite.passage carries only document_id and index, and cannot be resolved.

value.typeFields
text, booleantext
numbernumber
quantitynumber, unit
moneynumber, currency
timetime, plus time_end for a period, and the IANA zone its days are read in
entitytext. To write one, name the other entity with related_entity_id instead
noneNothing: the claim is its statement alone

Every write

Every POST on this page takes a request_id: a UUID you make, in its 36-character form. It makes retrying safe.

  • Retry with the same request_id and the same body and nothing is written twice. You get back the ids the first request wrote.
  • Send the same request_id with a different body and you get 422 request_id_reused.
  • A refused write leaves its request_id free to use again.
  • Two people or keys sending the same request_id make two separate requests.

Leaving a field out is the same as sending null. An unknown field is a 422.

Every write is recorded as made by the person or key that sent it, by name. A note (up to 2,000 characters) is kept with it and shown wherever the decision is.

Settle a conflict

When two sources disagree, POST /agents/{agent_id}/verdicts says which claim stands, or that both do.

curl -X POST "https://api.hyperstruck.com/agents/<AGENT_ID>/verdicts" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "request_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "statement_id": "55555555-5555-4555-8555-555555555555",
    "outcome": "stands",
    "note": "Confirmed by the practice manager on the phone."
  }'

statement_id is a claim on the conflicting line: its current.id or a beside[].id.

outcomeWhat happens
standsThis claim is the answer. On a detail that holds one value, every other live claim ends. On one that holds several, only the conflicts involving this claim are settled
bothEvery conflict involving this claim is settled with both claims standing

The response gives verdict_ids and the item of every line the decision changed. Settling a payment route nobody has confirmed confirms it, even when it has no conflict: send its statement_id with stands. Any other claim with no conflict to settle is 409 nothing_to_settle. An API key may not settle a conflict on a protected attribute (403 person_required).

Correct or add a claim

POST /agents/{agent_id}/statements records what is true now.

To correct a claim, name it in replaces. Every field you leave out is carried over from it:

{
  "request_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
  "replaces": "44444444-4444-4444-8444-444444444444",
  "value": { "type": "text", "text": "Wednesday mornings" },
  "note": "Rota change announced at the staff meeting."
}

To add a claim, leave out replaces and send entity_id, attribute_id and value:

{
  "request_id": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
  "entity_id": "22222222-2222-4222-8222-222222222222",
  "attribute_id": "88888888-8888-4888-8888-888888888888",
  "value": { "type": "text", "text": "Tuesday and Thursday mornings" },
  "as_of": "2026-10-27T00:00:00Z"
}

The attribute must already exist; list the attributes to find its id.

FieldMeaning
replacesThe claim this one corrects
entity_id, attribute_id, qualifiersThe line, when adding. When correcting, they must match the claim you replace
valueThe value, shaped by its type
as_ofWhen it holds from; default now. Not earlier than the start of the claim it replaces
mode, due_at, holder_entity_id, recipient_entity_idObligations only; see Obligations. Reads return the holder as holder
related_entity_idRelations only, where it is required: the other entity
noteWhy

On a detail that holds one value, correcting a value claim ends every other live claim on the line. Any other correction, such as one to an obligation, a reading, or a detail that holds several values, ends only the claim it replaces. The response gives the statement_id that now stands and the line's item.

Sending the value that is already there changes nothing

When the value you send matches a live value on a detail that holds one value, nothing new is written: the response gives the existing claim's id, and the line is unchanged. Text matches ignoring case and surrounding spaces; numbers, times, currencies and units must match exactly.

A refusal names the field at fault. Common ones: a field that does not fit the attribute's kind, a replaced claim that has already ended (409 not_current, listing what holds now), or the entity's identity, which only fixing who is who changes.

On a protected attribute, an API key's correction to a value is held for a person to approve instead of being refused, as long as it is not dated earlier than the claim now standing on the line. Any other change an API key sends to a protected attribute is 403 person_required.

Fix who is who

When documents name the same person or company in different ways, or one name covers two different people, a signed-in person can fix it:

RouteBodyWhat it does
POST /entities/{entity_id}/aliasesalias (1 to 300 characters)Adds another name for the entity, and answers with the entity and all its names. A name it already has changes nothing
POST /entities/{entity_id}/mergeintoThe two are one. Answers with surviving_id. Merging an entity into itself is a 422; merging a pair a person split apart is 409 identity_walled
POST /entities/{entity_id}/splitfrom, optionalThe two are not one. Without from, the merge that folded this entity into another is undone. Answers with entity_ids. Splitting an entity that was never merged changes nothing

After a merge, the old id keeps working on every read and reads as the entity it merged into.

Attributes

An attribute is the name of a detail, such as "clinic days" or "support plan". Hyperstruck creates them as it reads; these routes let you tidy them.

List the attributes

GET /agents/{agent_id}/attributes lists every attribute in name order, including folded ones.

ParameterMeaning
qNames containing this, ignoring case
kindOne kind
limit, cursor1 to 200, default 50

Each attribute carries id, name, kind, nature, protected, is_many_valued (whether it holds several values at once), and who folded it or set its nature, and when.

Fold one attribute into another

POST /attributes/{attribute_id}/fold with into says two names mean the same thing, such as "clinic days" and "clinic schedule". Their claims move onto into, and reads follow it. The two must be the same kind and both hold one value or both hold several, or the fold is 409 fold_refused with the reason.

Set an attribute's nature and protection

POST /attributes/{attribute_id}/nature takes nature and protected. Fields you leave out keep their value, and sending what the attribute already has changes nothing.

  • nature is standing for a detail that holds until it changes, or reading for a measurement where each one keeps its own date.
  • protected set to true means only a person can approve a change to its value. Protected details explains when to use it. An API key may set protected: true, and nothing else on this route; anything else it sends is 403 person_required. Setting protected: false on a protected attribute needs the right to change the agent (its home-space steward, a manager or an org admin); a person who may only curate gets 403 agent_write_required.

An attribute belongs to the whole agent, so these routes need write access to every space the agent keeps (403 all_spaces_required).

Erase an entity

POST /agents/{agent_id}/entities/{entity_id}/erasure, with no body, erases the entity, every claim about it, its other names and every decision about it. It works even while the agent's claims are still being rebuilt. The response is a receipt:

{
  "id": "99999999-9999-4999-8999-999999999999",
  "subject_kind": "entity",
  "subject_id": "22222222-2222-4222-8222-222222222222",
  "claims_erased": 14,
  "statements_erased": 11,
  "obligations_erased": 2,
  "relations_erased": 1,
  "verdicts_erased": 2,
  "aliases_erased": 1,
  "dossiers_rebuilt": 1,
  "claims_reopened": 0,
  "claims_reended": 0,
  "references_cleared": 3,
  "entities_unmerged": 0,
  "verdicts_written": 0,
  "insight_findings_deleted": 0,
  "residue": []
}

Most fields count what was removed: claims_erased is every claim, split into statements_erased, obligations_erased and relations_erased. These need a closer look:

FieldMeaning
claims_reopenedClaims about other entities that an erased claim had replaced, which hold again
references_clearedPlaces another claim named this entity, now cleared
insight_findings_deletedInsight findings removed because they named the entity or cited an erased claim
residueAnything the erasure could not finish that may still hold the entity's name or a value, such as learning_literals. Empty when nothing remains. Send the erasure again to finish it: the repeat clears what was left and answers 404

Add ?dry_run=true to see the receipt the erasure would give without erasing anything. It needs the same access as the erasure itself.

Keep the first receipt

Erasing an entity that is already erased answers 404, not the receipt again. If you lose the first response, list the entity's claims: a 404 there confirms it is gone.

Clear an agent's claims

DELETE /agents/{agent_id}/claims erases every claim the agent holds in one request.

{
  "wipe_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "agent_id": "3f0c1a52-8d4e-4b6a-9c1e-0a7d2b5e1f11",
  "entities_erased": 40,
  "claims_erased": 612,
  "attributes_removed": 31,
  "remaining_entities": 0,
  "has_residue": false,
  "request_ids": []
}

remaining_entities is always 0 and request_ids is always empty. The request accepts reason and ticket_ref, but neither is stored, so keep your own record of why you cleared it.

Limits

LimitValue
Claim reads per API key, or per signed-in person (every read on this page except a learning's claims and the attributes list, plus Claims search)60 a minute, failed reads included
Claim reads in progress at once, per account4 per server; more are refused straight away
limit on claims, dossier, changes, sources, what is left to decide, documents that changed something, and attributes1 to 200, default 50
limit on finding or listing entities1 to 50, default 10
limit on a learning's claims1 to 100, default 25
relations and owed on a dossierThe newest 25 each
note2,000 characters
cursor400 characters

Too many reads at once answers 429 (or 503 when the whole server is busy) with code claim_reads_busy. Going over 60 a minute answers 429 with code claim_reads_rate_limited. Both carry retry_after_seconds in the body and the same number in a Retry-After header: for the per-minute limit, it is the seconds left in the minute.

Errors

A refused write carries {"detail": {"code": ..., "message": ..., "field": ...}}, where field names the part of the request at fault. Some refused reads carry a plain message in detail instead.

StatusCodeWhen
400cursor_invalidA learning's claims list was sent a cursor that is not its own
403missing_scopeThe key lacks the scope the route needs
403curate_requiredreveal=true from a caller who may not curate the agent's claims
403person_requiredAn API key tried a decision only a person may make
403all_spaces_requiredAn attribute change without write access to every space the agent keeps
403agent_write_requiredRemoving an attribute's protection without the right to change the agent
404not_found, or a plain messageThe claim, entity or attribute is not held, or is in a space you cannot read (for a write, one you cannot write into)
409claims_not_replayedThe agent's claims are still being rebuilt (below)
409nothing_to_settleThe claim has no conflict to settle and is not a payment route waiting to be confirmed
409not_currentThe claim you tried to replace has already ended
409identity_walled, merge_cycle, split_not_adjacentThe merge or split contradicts an earlier one
409fold_refusedThe fold's target is missing, is itself folded, or differs in kind or in how many values it holds
409request_in_progressA request with the same request_id is being written right now; retry it
422invalid_requestA field is missing, malformed or does not fit; field names it
422invalid_cursorA read was sent a cursor handed out under another order, filter or entity
422invalid_filterFilters that do not fit together: order=due without kind=obligation, or due_before or due_after with an order other than due
422request_id_reusedThe request_id was already used for a different body
429, 503claim_reads_busyToo many reads at once; retry after Retry-After
429claim_reads_rate_limitedOver 60 reads this minute; retry after Retry-After
503claim_store_unavailableThe claims cannot be read right now; retry after Retry-After (5 seconds)

The claim store is still being rebuilt

An older agent may answer 409 claims_not_replayed on every claim read and write while its documents are read again into the current claim store. Once that finishes, it never answers it again. Agents created since never answer it, and erasure is never held back.