# 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 /<file>.html page. ## Errors {"error":"<msg>"} 400 bad fields | 401 auth | 403 admin/forbidden | 404 not found | 405 method | 409 keyId exists | 410 expired | 413 too large