# htmlslop - agent API reference
Upload HTML -> stable content-addressed URL. Aliases = repointable redirects.
Writes are authenticated: RFC 9421 signature (hmac-sha256) + RFC 9530 digest.
Base URL: https://htmlslop.com.
Reference signers (fetchable): /examples/sign.py , /examples/sign.js , /examples/sign.sh
## Headers (every write)
Content-Digest: sha-256=::
Signature-Input: sig1=
Signature: sig1=::
alg is always hmac-sha256; label is always sig1.
## params
("@method" "@path" "content-digest");created=;keyid="";nonce="";alg="hmac-sha256"
created = unquoted unix seconds. keyid/nonce/alg = quoted. Optional expires=.
## Signature base (bodied: POST/PUT)
Exactly these lines joined by "\n", NO trailing newline:
"@method":
"@path":
"content-digest":
"@signature-params":
Signature = base64(hmac_sha256(secret, base)).
DELETE (no body): drop the content-digest line AND "content-digest" from params;
send no Content-Digest header.
## Rules (each violation = opaque 401)
1. HMAC key = raw secret STRING bytes. Never hex/base64-decode it first.
2. No trailing newline on the base.
3. Component order = the order in params. Names lowercase, double-quoted.
Only @method, @path, content-digest supported.
4. @path = path only, no scheme/host/query. @authority not covered.
5. Digest over the EXACT body bytes you send. No JSON canonicalization. Sign and
send identical bytes (don't re-serialize after signing).
6. content-digest base line = full verbatim header value (incl. "sha-256=:" ... ":").
7. @signature-params base line = verbatim params text (same as in Signature-Input).
8. Freshness: |now - created| <= 300s.
9. nonce: fresh per request, single-use, 8-200 chars [A-Za-z0-9_\-+/=]. Reuse=401.
Server check order (maps each 401): parse -> alg -> keyid/created/nonce present ->
freshness -> digest vs body -> key active(/admin) -> hmac -> nonce claim (replay).
## Test vector (verify your signer offline before calling the API)
INPUT
keyId=vector-key
secret=a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90
method=POST path=/api/v1/upload created=1735689600 nonce=test-nonce-0001
body={"html":"Hello, htmlslop
","title":"Test Vector"}
EXPECTED Content-Digest
sha-256=:H5vxubjh+a+91POPZuaod42Q4khXQLVlyrHGh5grMMQ=:
EXPECTED base (between markers; no leading/trailing newline)
-----BEGIN-----
"@method": POST
"@path": /api/v1/upload
"content-digest": sha-256=:H5vxubjh+a+91POPZuaod42Q4khXQLVlyrHGh5grMMQ=:
"@signature-params": ("@method" "@path" "content-digest");created=1735689600;keyid="vector-key";nonce="test-nonce-0001";alg="hmac-sha256"
-----END-----
EXPECTED Signature-Input
sig1=("@method" "@path" "content-digest");created=1735689600;keyid="vector-key";nonce="test-nonce-0001";alg="hmac-sha256"
EXPECTED Signature
sig1=:QM7kdnE4VTZxPndT9nMNU2rG8HFeAwrCx0zrIK4ZQts=:
(secret is 64 hex chars used as 64 ASCII key bytes, NOT 32 decoded bytes - see rule 1.)
## Endpoints (JSON in/out; auth on all)
POST /api/v1/upload (any key)
{html, title, ttl?} html<=5242880 B; ttl[60,31536000] default 604800
ttl may not exceed the signing key's max_ttl_seconds (default 604800) -> else 403.
201 {status,filename,url,expiresAt,ttl}
url=/-.html, immutable. Same html+title=same url.
POST /api/v1/aliases (any key)
{title, target} target=existing /-.html
201 {status,aliasId,url} url 302->target, never cached.
PUT /api/v1/aliases/{id} (any key) {target} repoint, url unchanged.
DELETE /api/v1/aliases/{id} (any key) no body.
POST /api/v1/keys (ADMIN) {keyId, secret?(>=32), isAdmin?, maxTtl?(default 604800)}
201 {keyId,isAdmin,maxTtl,secret?(once)} maxTtl clamped to [60,31536000].
DELETE /api/v1/keys/{keyId} (ADMIN) deactivate; not self.
GET /api/v1 (no auth) descriptor. GET / docs. GET /.html page.
## Errors {"error":""}
400 bad fields | 401 auth | 403 admin/forbidden | 404 not found | 405 method |
409 keyId exists | 410 expired | 413 too large