HTTP API
All bodies are JSON. Keys and signatures are hex, and notes are unpadded
URL-safe base64. {mint} is a mint’s issuer key, in hex. A note names its
issuer, so /v1/verify, /v1/transfer and /v1/burn take no mint in their
path: they go to the mint whose key signed the note, or the first input
note.
The request types are the library’s own (TransferRequest, BurnRequest,
MintRequest, Note, PublicKey), so a Rust client signs with the library
and posts the JSON it serializes to. worgl-client and the TypeScript SDK
make every call below for you.
Routes
Section titled “Routes”| Method | Path | Token | Body | Answer |
|---|---|---|---|---|
| GET | /healthz |
ok |
||
| GET | /readyz |
ok, or 503 naming the mints that do not serve |
||
| POST | /v1/verify |
{"note"} |
serial, amount, state, position, hops left | |
| POST | /v1/transfer |
{"inputs": [{"note", "signature"}], "outputs": [{"amount", "holder"}]} |
{"notes"} |
|
| POST | /v1/burn |
{"inputs": [{"note", "signature"}]} |
{"value"} burned |
|
| GET | /v1/mints/{mint}/info |
layers, supply, policy, rotation schedule, paused | ||
| GET | /v1/mints/{mint}/keys |
the live generations’ keys and their certificates | ||
| POST | /v1/mints/{mint}/mint |
{"authority", "sequence", "amount", "holder", "signature"} |
{"note"} |
|
| POST | /v1/mints/{mint}/rotate |
mint | what was destroyed, and the new generation’s key | |
| POST | /v1/mints/{mint}/pause |
mint | {"paused": true} |
|
| POST | /v1/mints/{mint}/resume |
mint | {"paused": false} |
|
| GET | /v1/admin/mints |
operator | {"mints"} |
|
| POST | /v1/admin/mints |
operator | {"ledger", "policy", "rotate_every"} |
{"mint", "token"} |
Routes with a token need Authorization: Bearer <token>: a mint’s routes
take that mint’s own admin token, and /v1/admin routes the server’s
operator token. No token can mint: a mint request needs a signature from a
key the mint’s policy lists in mint_authorities.
Request bodies are limited to 64 KiB.
Errors
Section titled “Errors”Errors answer with a status code and a body like
{"error": "spent", "message": "note 3:17 is already spent"}. The error
code is stable; the message is for people. A 503 busy answer also says in
its Retry-After header how many seconds to wait before sending the request
again. See Error codes.
Health and readiness
Section titled “Health and readiness”GET /healthz answers ok for as long as the server runs, whatever its
mints are doing. GET /readyz answers ok only while every mint serves.
Otherwise it answers 503 with the storage code and every mint that does
not, so one such mint makes the whole server answer 503:
{"error":"storage","message":"not every mint is serving","mints":[{"mint":"7e8e…5caf","state":"failing"}]}state |
Meaning |
|---|---|
failing |
A change did not reach the mint’s disk, as on a full disk, and none has since. The mint answers reads from what reached its disk, and changes are answered 500 storage while the disk takes no writes. It serves again once a change reaches its disk, so it stays failing after the disk has room until a change comes. |
ahead |
A change did not reach the mint’s disk, and the mint could not reread its state from the disk since. It refuses changes, answers verify and info with 503 storage, and tries again at its next request. |
stopped |
A change to the mint’s log failed and could not be undone, as when the log could not be cut back after a failed write, so only a restart serves the mint again. On a cluster member, also once the member’s copy of the mint has stopped. |
See Running a server.
Verify
Section titled “Verify”POST /v1/verify with {"note": "<base64>"} checks a note’s signature and
answers its state. It works while the mint is paused.
{"serial":{"generation":2,"index":999119},"amount":8,"state":"spent"}state is live, spent, expired or unissued. A live note also has
position (its layer, 0 for the first), hops_left (transfers its lineage
can still make before the next rotation; 0 means frozen) and
expires_after_rotations (untouched, it expires in this many rotations
from now).
Transfer
Section titled “Transfer”POST /v1/transfer spends input notes and issues new ones. Each input
carries its holder’s signature over one message that names every input and
every output; see Note format. The answer is
{"notes": [...]}, one per output, in order. The outputs land one layer past
the furthest input.
Sending the same request again is safe: the mint keeps a receipt of recent
transfers by the hash of their signed message and answers a resent one with
the same notes, even after a rotation has dropped its inputs’ layer, until
those notes expire. It keeps each receipt for at least the policy’s
receipt_min_age_secs; while it holds receipt_capacity receipts all
younger than that, or for a while after a restart counted so, it answers a
new transfer or burn 503 busy and does nothing.
POST /v1/burn destroys its input notes and issues nothing, so value that is
redeemed leaves circulation at once. Burns follow the same input limit as
transfers and work on a note frozen in the last layer. Their signatures are
made in their own domain, so a burn signature can never be replayed as a
transfer or the other way round. A resent burn is answered with the same
value and destroys nothing more.
Mint info
Section titled “Mint info”GET /v1/mints/{mint}/info:
{ "issuer": "7e8e…5caf", "sequence": 2, "mint_layer": 2, "layers": [ {"position": 0, "generation": 0, "issued": 0, "live": 0, "capacity": 1048576, "value": 0}, {"position": 1, "generation": 1, "issued": 0, "live": 0, "capacity": 1048576, "value": 0}, {"position": 2, "generation": 2, "issued": 1, "live": 0, "capacity": 1048576, "value": 0}, {"position": 3, "generation": 3, "issued": 4, "live": 4, "capacity": 1048576, "value": 8} ], "supply": {"minted": 8, "destroyed": 0, "burned": 0, "outstanding": 8}, "policy": {"mint_layer": null, "max_inputs": 1, "max_outputs": 16, "denominations": "powers_of_two", "receipt_capacity": 1048576, "receipt_min_age_secs": 128, "mint_authorities": ["b192…5673"]}, "rotation": null, "paused": false}| Field | Meaning |
|---|---|
sequence |
Events applied since the mint was created. |
mint_layer |
The position new notes are minted into. |
layers |
Every layer, first to last: bits handed out (issued), notes still live, capacity and the live notes’ value. |
supply |
Value minted, destroyed by rotations and burned; outstanding = minted − destroyed − burned. |
policy |
The rules in force. See Policy fields. |
rotation |
{"period_secs", "next_at"}, or null when the mint rotates only on request. next_at is the Unix time, in seconds, of the next scheduled rotation, read with layers: an answer that shows a rotation’s layers names the rotation after it. |
paused |
Whether the mint is paused. |
The answer’s Date header is the server’s time when it answered. The SDKs’
purses and worgl count the time left before next_at from it, not by the
client’s clock, and a cluster member that passes the request on to the
leader relays the leader’s. A browser page reads it on an answer from
another origin only if the answer carries
Access-Control-Expose-Headers: Date, which the server does not send: the
proxy that adds CORS headers in front of it must add that one too.
GET /v1/mints/{mint}/keys answers the keys that sign the live generations’
notes, each certified by the issuer key:
{"issuer":"7e8e…5caf","oldest":0,"keys":[{"generation":0,"key":"7af7…7bff","signature":"4674…5300"}, …]}Anyone who knows the issuer key can check a note offline with these. See Note format.
POST /v1/mints/{mint}/mint takes a request signed by a mint authority and
answers {"note": "<base64>"}. worgl-server sign-mint prints one, ready to
post; see Mint authorities and keys.
Rotate, pause and resume
Section titled “Rotate, pause and resume”POST /v1/mints/{mint}/rotate answers what the rotation did:
{"dropped":0,"created":4,"destroyed_notes":0,"destroyed_value":0,"key":{"generation":4,"key":"5de1…c4ce","signature":"9699…8e08"}}Unlike a transfer, a rotation sent again is made again; see Sending a request again.
pause and resume answer {"paused": true} and {"paused": false}. See
Pausing a mint.
Creating mints
Section titled “Creating mints”POST /v1/admin/mints takes a ledger shape, a policy and an optional
period, and answers the new mint’s issuer key and admin token:
{"ledger": {"layers": 4, "layer_size": 1048576}, "policy": {"mint_authorities": ["<public key>"]}, "rotate_every": 86400}The policy must list a mint authority, since the server never holds one.
Missing policy fields take their defaults. Without rotate_every the mint
rotates only on request. GET /v1/admin/mints lists every mint’s issuer key.
On a cluster
Section titled “On a cluster”Any member takes any request and passes it on to the member that leads the
mint. While a mint has no leader its requests are answered 503 no_leader;
see Running a cluster. Clients need one address
that covers every member, such as a proxy in front of them; see
The address clients use.
v0.1Not yet independently security reviewed. Don't use it to hold anything you can't afford to lose.MIT or Apache-2.0