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:
| Scope | What it allows |
|---|---|
claims:read | Every read on this page, and Claims search |
claims:correct | Settling a conflict, correcting or adding a claim, and marking an attribute protected |
claims:curate | Everything claims:correct allows; for a signed-in person, also fixing who is who and changing attributes |
claims:erase | Erasing 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.
| Parameter | Meaning |
|---|---|
entity | Only this entity's lines. An entity that was merged into another reads as the one it merged into |
scope | This entity and every entity one standing relation away from it, in either direction |
state | settled, unanswered, conflicting, open, done or ended (what they mean) |
kind | value, reading, obligation, event, view or relation |
attribute | Only lines of these attributes; repeat it for several |
owner | Obligations this entity is recorded as owing |
due_before, due_after | Obligations due before this time, or at or after it |
due | dated for obligations with a due date, none for those without |
document | Only lines with a claim that cites this document (the id a claim's document carries) |
container | Only lines citing a document from this thread or channel |
raised_after, raised_before | Only lines whose current claim was recorded at or after, or before, this time |
order | key (the default), raised for newest first, or due for soonest due first. due needs kind=obligation; due_before and due_after imply it |
as_of | The claims as they stood at that time |
detail | How much each line carries: concise (the default), cited or full (below) |
limit | 1 to 200; default 50 |
cursor | The 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.
| Field | Meaning |
|---|---|
entity | The entity |
claims | Its lines, a page at a time (limit 1 to 200, default 50, and cursor) |
aliases | The other names it answers to |
fields | Each attribute it holds, with how many lines it holds under it |
relations | Its links to other entities, the newest 25 |
owed | Open obligations filed under it, grouped by who owes them, then by due date, the newest 25 |
possible_duplicates | Open questions whether it is the same as another entity, each naming both, the rule that raised it and a reason_sentence in plain words |
more | truncated is true when relations or owed was cut, and hint says where to list the rest |
cursor | The 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:
detail | Each line carries |
|---|---|
concise (default) | The current claim; on a conflicting line, the claims competing with it; a run of readings summarized in series |
cited | Everything 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 |
full | Everything 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.
| Field | Meaning |
|---|---|
key | Names the source; pass it back as key to read that one alone |
channel, container, container_name | The route the documents came by, and the thread they belong to |
document_id | The document, when the source is a single one |
title, author, kind | The source's title, author and kind |
documents, claims | How many documents the source covers, and how many claims they gave |
first_at, latest_at | The dates of its first and latest documents |
latest_doc_id | Your 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.
| Parameter | Meaning |
|---|---|
kind | conflict or duplicate |
entity | Only this entity's own decisions |
scope | This entity and every entity one standing relation away from it |
raised_after | Only decisions raised at or after this time |
order | newest (the default) or oldest |
group | none (the default), document, entity or field; groups then counts each group across the whole filter |
limit, cursor | 1 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:
| Field | Counts the lines where |
|---|---|
established | Its claim is the current one |
ended | It ended or replaced an earlier claim |
undecided | One side of a conflict cites it |
since_replaced | A later document replaced the value it gave |
since_followed | A later document gave a different value without announcing a change |
since_ended | The 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.
| Field | Meaning |
|---|---|
token | Where the detail sits in the learning |
distilled_from | claim_id and state of the claim it first came from: in_force, ended, or gone when that claim no longer exists |
resolves_to.state | The 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.value | The value your agent would see, or null when none can be shown |
resolves_to.is_value_withheld | true 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_safety | true when a safety learning keeps showing the earlier value until a person accepts the change |
last_literal | The 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.
| Field | Meaning |
|---|---|
id | The line: <entity id>|<attribute id>|<qualifiers> |
entity | id, name, type and category |
attribute | id, name, kind, nature and protected |
qualifiers | What narrows the line, such as {"room": "B"}, or {} |
state | settled, unanswered, conflicting, open, done or ended (what they mean) |
conflict_kind | Why a conflicting line waits (the reasons), or null |
reason_sentence | The same reason in plain words, or null |
conflicts_with | For a conflict between two lines, the other line's id, entity and attribute; otherwise null |
current | The claim that holds now, or null when nothing does |
beside | Other live claims on the line: the ones competing on a conflicting line, and at cited or full detail every other live claim |
history | Earlier claims, newest first, at full detail only |
series | For a line of readings at concise detail: the count, from and to of the readings summarized behind current |
sources | Each document_id with the passage (a document) or step (a run) the claims come from, once each |
word_share, cosine | How well the claim matches a question; filled only by Claims search |
Each claim in current, beside and history carries:
| Field | Meaning |
|---|---|
id | The claim's id, which corrections and verdicts take |
value | type plus the fields that type uses (below) |
statement | The 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_until | When it holds from, and when it ended; valid_until is null while it holds |
recorded_at | When Hyperstruck recorded it |
cite | Where it was said: {"passage": {...}} for a document, {"step": {...}} for a run |
excerpt | A run step's words, at cited and full detail; always null for a document |
document | The source's id, kind, date, author and title, the channel and container_name it came by, and the zone its times were read in |
label | For 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 |
support | How many passages said it, the first included |
also_stated_by | The other passages that said it; empty at concise detail |
holder, due_at | For an obligation: the entity it is owed by (id, name, type, category) and when it is due |
related | For 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.type | Fields |
|---|---|
text, boolean | text |
number | number |
quantity | number, unit |
money | number, currency |
time | time, plus time_end for a period, and the IANA zone its days are read in |
entity | text. To write one, name the other entity with related_entity_id instead |
none | Nothing: 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_idand the same body and nothing is written twice. You get back the ids the first request wrote. - Send the same
request_idwith a different body and you get422 request_id_reused. - A refused write leaves its
request_idfree to use again. - Two people or keys sending the same
request_idmake 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.
outcome | What happens |
|---|---|
stands | This 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 |
both | Every 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.
| Field | Meaning |
|---|---|
replaces | The claim this one corrects |
entity_id, attribute_id, qualifiers | The line, when adding. When correcting, they must match the claim you replace |
value | The value, shaped by its type |
as_of | When it holds from; default now. Not earlier than the start of the claim it replaces |
mode, due_at, holder_entity_id, recipient_entity_id | Obligations only; see Obligations. Reads return the holder as holder |
related_entity_id | Relations only, where it is required: the other entity |
note | Why |
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:
| Route | Body | What it does |
|---|---|---|
POST /entities/{entity_id}/aliases | alias (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}/merge | into | The 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}/split | from, optional | The 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.
| Parameter | Meaning |
|---|---|
q | Names containing this, ignoring case |
kind | One kind |
limit, cursor | 1 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.
natureisstandingfor a detail that holds until it changes, orreadingfor a measurement where each one keeps its own date.protectedset totruemeans only a person can approve a change to its value. Protected details explains when to use it. An API key may setprotected: true, and nothing else on this route; anything else it sends is403 person_required. Settingprotected: falseon 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 gets403 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:
| Field | Meaning |
|---|---|
claims_reopened | Claims about other entities that an erased claim had replaced, which hold again |
references_cleared | Places another claim named this entity, now cleared |
insight_findings_deleted | Insight findings removed because they named the entity or cited an erased claim |
residue | Anything 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
| Limit | Value |
|---|---|
| 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 account | 4 per server; more are refused straight away |
limit on claims, dossier, changes, sources, what is left to decide, documents that changed something, and attributes | 1 to 200, default 50 |
limit on finding or listing entities | 1 to 50, default 10 |
limit on a learning's claims | 1 to 100, default 25 |
relations and owed on a dossier | The newest 25 each |
note | 2,000 characters |
cursor | 400 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.
| Status | Code | When |
|---|---|---|
400 | cursor_invalid | A learning's claims list was sent a cursor that is not its own |
403 | missing_scope | The key lacks the scope the route needs |
403 | curate_required | reveal=true from a caller who may not curate the agent's claims |
403 | person_required | An API key tried a decision only a person may make |
403 | all_spaces_required | An attribute change without write access to every space the agent keeps |
403 | agent_write_required | Removing an attribute's protection without the right to change the agent |
404 | not_found, or a plain message | The claim, entity or attribute is not held, or is in a space you cannot read (for a write, one you cannot write into) |
409 | claims_not_replayed | The agent's claims are still being rebuilt (below) |
409 | nothing_to_settle | The claim has no conflict to settle and is not a payment route waiting to be confirmed |
409 | not_current | The claim you tried to replace has already ended |
409 | identity_walled, merge_cycle, split_not_adjacent | The merge or split contradicts an earlier one |
409 | fold_refused | The fold's target is missing, is itself folded, or differs in kind or in how many values it holds |
409 | request_in_progress | A request with the same request_id is being written right now; retry it |
422 | invalid_request | A field is missing, malformed or does not fit; field names it |
422 | invalid_cursor | A read was sent a cursor handed out under another order, filter or entity |
422 | invalid_filter | Filters that do not fit together: order=due without kind=obligation, or due_before or due_after with an order other than due |
422 | request_id_reused | The request_id was already used for a different body |
429, 503 | claim_reads_busy | Too many reads at once; retry after Retry-After |
429 | claim_reads_rate_limited | Over 60 reads this minute; retry after Retry-After |
503 | claim_store_unavailable | The 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.