API worked examples
Copy-and-run examples for the calls most integrations need: list, read, create, update, delete and search over REST v2, then harvesting over OAI-PMH and ResourceSync. Every call on this page is run by the test packages/ahg-api/tests/Feature/WorkedExamplesTest.php against a real Heratio install, so the calls work as shown. The ids, slugs and counts in the sample responses are only illustrations.
Replace https://heratio.example.org with your own Heratio address and ahg_live_XXX with your key. For how to get a key and what its scopes allow, see REST API - AI assistants; the short version is /api/keys once you are signed in.
Before you start
Every REST v2 call except the discovery document needs a key, sent in X-API-Key (or Authorization: Bearer). Responses are JSON in one envelope:
{ "success": true, "data": { ... }, "timestamp": "2026-10-27T10:15:00+02:00" }
Errors use the same shape with "success": false, an error and a message. The status codes you will meet:
| Status | Meaning |
|---|---|
401 |
No key, or a key that is not valid |
403 |
The key lacks the scope (a read key trying to write), or it is a public-catalogue key on a staff route |
404 |
Not found, or not visible to this key (drafts, embargoed and restricted records look the same as missing ones) |
422 |
The request body failed validation; the response lists the fields |
429 |
Rate limit reached; wait the number of seconds in Retry-After |
: REST v2 status codes
1. Discover
No key needed. The answer lists the endpoints this install offers.
curl "https://heratio.example.org/api/v2"
2. List descriptions, a page at a time
curl -H "X-API-Key: ahg_live_XXX" \
"https://heratio.example.org/api/v2/descriptions?limit=2&sort=updated"
{
"success": true,
"data": {
"total": 1200, "limit": 2, "skip": 0,
"results": [
{ "id": 1001, "slug": "municipal-archives", "identifier": "MA",
"title": "Municipal Archives", "level_of_description": "Fonds",
"repository": "City Archives", "parent_id": null,
"created_at": "...", "updated_at": "..." }
]
},
"meta": { "total": 1200, "page": 1, "limit": 2, "last_page": 600 },
"links": { "self": ".../api/v2/descriptions?page=1&limit=2", "next": ".../api/v2/descriptions?page=2&limit=2" }
}
Follow links.next until it is absent. limit goes up to 100. Narrow the list with repository=<slug>, level=<term id> or parent=<id>; sort with sort=updated|title|identifier|archival and sort_direction=asc|desc.
3. Create a description
Needs the write scope. Only title is required; send the ISAD(G) fields you have.
curl -X POST -H "X-API-Key: ahg_live_XXX" -H "Content-Type: application/json" \
"https://heratio.example.org/api/v2/descriptions" \
-d '{
"title": "Harbour works correspondence",
"identifier": "WE-001",
"parent_slug": "municipal-archives",
"scope_and_content": "Letters about the harbour works.",
"publication_status": "draft",
"dates": [{ "type": "creation", "start_date": "1890-01-01", "end_date": "1899-12-31", "date_display": "1890s" }]
}'
{ "success": true, "data": { "id": 1275401, "slug": "harbour-works-correspondence", "title": "Harbour works correspondence" } }
The status is 201. Keep the slug: it is how every later call names the record. Leave out parent_slug to create a top-level description. New records are drafts unless you send "publication_status": "published", and a draft is invisible to public keys.
4. Read it back
curl -H "X-API-Key: ahg_live_XXX" \
"https://heratio.example.org/api/v2/descriptions/harbour-works-correspondence"
The full record comes back, with its events, creators, access points, notes, digital objects, hierarchy and a rights block (the rights statements, licences and Local Contexts labels in force). Add ?full=false for the core fields only.
5. Update a field
Needs write. Send only the fields you are changing; PUT and PATCH behave the same.
curl -X PATCH -H "X-API-Key: ahg_live_XXX" -H "Content-Type: application/json" \
"https://heratio.example.org/api/v2/descriptions/harbour-works-correspondence" \
-d '{ "scope_and_content": "Letters and plans about the harbour works." }'
{ "success": true, "data": { "id": 1275401, "slug": "harbour-works-correspondence", "message": "Description updated." } }
If you would rather an archivist approve the change first, propose it instead: POST /api/v2/descriptions/{slug}/proposals queues it for review. See REST API - AI assistants.
6. Search
curl -X POST -H "X-API-Key: ahg_live_XXX" -H "Content-Type: application/json" \
"https://heratio.example.org/api/v2/search" \
-d '{ "query": "harbour works", "limit": 5, "filters": { "repository": "municipal-archives" } }'
The matches are in data.results. Search returns published records only.
7. Delete
Needs the delete scope.
curl -X DELETE -H "X-API-Key: ahg_live_XXX" \
"https://heratio.example.org/api/v2/descriptions/harbour-works-correspondence"
{ "success": true, "data": { "id": 1275401, "slug": "harbour-works-correspondence", "message": "Description deleted." } }
A read of the same slug now answers 404.
8. Harvest over OAI-PMH
No key needed unless your administrator has turned OAI authentication on, in which case send a key with the oai scope. The endpoint is /oai.
curl "https://heratio.example.org/oai?verb=Identify"
curl "https://heratio.example.org/oai?verb=ListMetadataFormats"
curl "https://heratio.example.org/oai?verb=ListRecords&metadataPrefix=oai_dc"
The formats on offer are oai_dc, oai_ead, oai_ead3, mods, marcxml and rico. A long list comes in pages: each page ends with a <resumptionToken>, and you ask for the next page with that token alone.
curl "https://heratio.example.org/oai?verb=ListRecords&resumptionToken=TOKEN-FROM-THE-LAST-PAGE"
A full harvest in a few lines of Python:
import re, urllib.request, urllib.parse
base = "https://heratio.example.org/oai"
params = {"verb": "ListRecords", "metadataPrefix": "oai_dc"}
while True:
xml = urllib.request.urlopen(base + "?" + urllib.parse.urlencode(params)).read().decode()
# ... store or parse the <record> elements here ...
token = re.search(r"<resumptionToken[^>]*>([^<]+)</resumptionToken>", xml)
if not token:
break
params = {"verb": "ListRecords", "resumptionToken": token.group(1)}
For the next harvest, add from=<the date of your last harvest> to fetch only what changed. If your install advertises deletions, a deleted record comes back with status="deleted" in its header. More in OAI-PMH Metadata Harvesting.
9. Keep a copy in step with ResourceSync
ResourceSync (NISO Z39.99) suits a mirror that should follow every change, not just metadata records. Start from the discovery file and follow its links to the capability list and the change list:
curl "https://heratio.example.org/.well-known/resourcesync"
curl "https://heratio.example.org/resourcesync/capabilitylist.xml"
More in Open Data: ResourceSync.
GraphQL
Heratio's GraphQL endpoint is for signed-in staff, from the playground at Admin > GraphQL (/admin/graphql/playground). It does not accept API keys, so integrations and AI agents should use REST v2. Query examples are in the GraphQL API User Guide.
Rate limits
| Door | Limit |
|---|---|
| REST v2 | Per key: an hourly limit (1,000 by default) and, if set, a daily limit |
| REST v1 (no key) | 60 requests a minute per address |
| Browse pages on the website | Shared across all visitors; see Bots, crawlers and search engines |
: Rate limits
Every v2 response carries X-RateLimit-Limit and X-RateLimit-Remaining. Past a limit the answer is 429 with Retry-After in seconds: wait that long, then carry on.