API¶
The specification below is generated from the implementation at build time, so it cannot drift from what the server actually does.
A running instance also serves it: Swagger UI at /api/docs, ReDoc at /api/redoc,
and the raw document at /api/openapi.json.
The endpoints a role exposes¶
A minter has no resolution endpoint, and a resolver has no minting endpoint. They are separate processes so they can scale separately and the resolver can be pointed at a read-only replica.
What the schema will not tell you¶
Minting is idempotent if you ask it to be. Send a request_id and a repeat of the
same request returns the ARK you already have, rather than minting another. Loading
tens of thousands of records means a connection will break somewhere; without a
receipt, resending would leave behind identifiers nobody points at — and ARK does not
allow those to be reclaimed.
The shoulder in a request cannot widen anything. Omit it and the organisation's default is used; name one and the only question asked is whether it is already inside your reach.
A delegated shoulder answers 307, not a proxied mint. If minting for that namespace happens elsewhere, you are told where to go. arkhe does not call the other minter for you: a lost response would leave an ARK minted over there that this ledger has never seen.
A name can be minted before it is published, and deleted while it is. Send
{"reserve": true} to POST /api/mint and a public resolver does not resolve it — it
answers as it does for a name it has never seen (a resolver inside a closed network does,
with ARKHE_RESOLVE_UNPUBLISHED). POST /api/publish starts resolution
and closes the door on deleting it outright: POST /api/delete then answers 409. A
withdrawn name is never assigned again. See
Invariants.
Publication can be withdrawn, and taken up again. POST /api/unpublish (ark:unpublish)
leaves the row and stops the ARK resolving; POST /api/publish puts it back. Both act
within the caller's own reach — an organisation inside its own shoulder, a NAAN
administrator inside its NAAN. Because the name has been out in the world, unpublishing
needs a reason and confirm repeating the ARK, and so does deleting anything that has
ever been published. The weight follows the name's history, not the caller's rank.
An abandoned batch goes in one request. POST /api/delete/bulk (ark:delete)
deletes several ARKs that were never published — reserving in bulk is a way of
working, so throwing the batch away has to be as cheap as minting it was. One row that
is published, or that has ever been published, fails the whole request (409); that
name goes through POST /api/delete on its own, with a reason and the ARK typed again.
POST /api/purge does both steps at once (ark:purge, within your reach) — for a
removal order, or data that should never have been published — because without a way
out someone deletes rows straight from the database. It requires a reason, confirm
must repeat the ARK, and the whole thing is audited. The name is not freed, so a
stale reference gets 404 and never a different object.
Bulk operations do not partially apply. If one row in a bulk update is missing or out of reach, the whole request fails. arklet zipped an unordered query result against the input and could write one record's values onto another.
Resolution¶
Resolution is not in the OpenAPI document in a useful form — it is one route with behaviour that depends on the suffix.
| Request | Answer |
|---|---|
/ark:99999/x9tn1qkq2g7 |
302 to the target, or a description if there is none |
/ark:99999/x9tn1qkq2g7/page/3 |
302 to target/page/3 — suffix passthrough, no record of its own |
/ark:99999/x9tn1qkq2g7? |
ERC/ANVL kernel — who, what, when, where |
/ark:99999/x9tn1qkq2g7?? |
The above plus the persistence statement |
/ark:99999/x9tn1qkq2g7?info |
The same again — as a page, as JSON or as ANVL, by Accept |
/ark:99999/x9tn1qkq2g7?json |
The JSON of ?info, named directly |
/ark:12345/… (unknown NAAN) |
302 to the global resolver |
/.well-known/ark |
text/plain: the resolver's root path, ending in / |
/.well-known/ark with Accept: application/json |
What this resolver holds, and where minting happens if elsewhere |
A bare ? cannot be distinguished from no query string at the protocol level — even
in ASGI. Set ARKHE_RAW_URI_HEADER if something in front passes the raw URI.
Every error carries a code (ARKHE-1011), an English message, and a structured
detail. Match on the code, not on the wording — see
Errors for the full list.
THUMP headers and where¶
Every answer the resolver gives about an identifier — ?, ??, ?info, ?json, a
description with no target, a hold, a 404 — carries the two headers from §5.2:
The Link is what tells a recipient who knows nothing about inflections that the
response describes the uninflected ARK rather than being a representation of the URL
it fetched. The specification's own example writes it <…> rel="describes";; that is
not a valid RFC 8288 link value, so arkhe
emits the well-formed <…>; rel="describes" — the same assertion, in a form standard
parsers can read. Redirects carry neither header: a redirect is the access service, not
an answer about the identifier.
In the ERC, where is the ARK, not the target — §5.1.2 defines it as "the
long-term identifier as opposed to a transient redirect target". Changing where an ARK
points does not change its where; that is the whole value of the element. The current
target is published as redirect, outside the kernel, and only when there is one.
?info answers in whichever medium you ask for¶
§5.2 says the form of a THUMP response is indicated by the returned content type,
so ?info negotiates: a page by default, application/json for a program,
text/plain for the ANVL that ?? returns. The content is the same description and
permanence declaration each time — §5 puts both in one ?info. ?json remains as a
way to name the JSON directly, and both carry Vary: Accept, Accept-Language.
The page itself is translated (?info&lang=en, Accept-Language, then the default).
?lang= cannot be used — on this endpoint the query string is the inflection, so
the language goes after an &.
%-encoded characters¶
A reserved character (%, -, ., /) may be %-encoded to conceal its reserved
meaning — %2F is the only way to write "there is a slash here, but it does not
separate components". So ark:99999/x9tn1qkq2g7%2Fc2 and ark:99999/x9tn1qkq2g7/c2
are different identifiers, and the specification forbids the encoded form from ever
appearing decoded (draft-kunze-ark-42 §3.2). arkhe reads the still-encoded path from
the ASGI raw_path and normalises only the hex case (%2f → %2F, step 5); it never
decodes.
Anything in front must pass the encoding through. With nginx, proxy_pass without
a URI part (proxy_pass http://backend;) — adding a path makes nginx re-encode the
decoded one. With Apache, AllowEncodedSlashes NoDecode. Where a proxy normalises
%2F anyway, the encoding cannot be recovered and such names will not resolve.
/.well-known/ark¶
draft-kunze-ark-42 §5.6 registers ark in the Well-Known URIs registry
(RFC 8615) and defines the answer as plain
text containing the resolver's root path, ending in / — append a compact ARK to it
and you have a resolution request. A client that sends no Accept, or */*, gets
that; answering such a client with JSON would make the host look like it has no ARK
resolver at all.
arkhe's own inventory — the namespaces it holds, delegated shoulders and their
minter, and any held namespace — is the JSON representation of the same URL, so ask
for it by name:
Both carry Vary: Accept. The path comes from the ASGI root_path, so if you mount
arkhe under a prefix, set it (uvicorn --root-path /pid) or the answer will point at a
door that is not there.