{"revision":"0ed62d2c742f8e20","language":"en","entry":{"id":"publish-statement","kind":"endpoint","group":"manage","title":"Publish a signed statement","method":"POST","path":"/v1/identities/{n}/statements","paths":[],"summary":"A person or an organisation publishes a statement with a title, a text and an optional link, or retracts an earlier one. The log’s witnesses date it.","response":"201 with the statement as the card’s pages give it, its log entry and proof included.","parameters":[{"name":"n","description":"Permanent positive card number.","in":"path","required":true}],"sample":"/v1/identities/1/statements","access":"signed","base":"directory","notes":["A person signs with one of its device keys; an organisation through an owner or an admin, with a key of their own card, as its versions are signed.","At most 10 statements per card in 24 hours, retractions included. GET on this path is the page of the staff’s statements (from, size)."],"errors":["400 invalid_json","400 invalid_nonce","400 invalid_parameter","400 invalid_signature_alg","400 invalid_timestamp","400 wrong_format","401 invalid_signature","401 missing_signature","401 stale_timestamp","403 identity_banned","403 key_banned","403 key_mismatch","403 role_not_allowed","404 not_found","409 already_retracted","409 erase_pending","409 replayed_nonce","409 signer_stale","410 erased","413 payload_too_large","422 bad_entry","422 invalid_statement","429 rate_limited","429 statement_quota","500 internal_error","503 busy","503 log_unavailable"],"related":["card-statements","statement","statements-feed","signed-requests"],"sections":[{"kind":"code","language":"shell","code":"curl -fsS -X POST \\\n  -H \"X-Public-Key: $PUBLIC_KEY\" \\\n  -H \"X-Signature: $REQUEST_SIGNATURE\" --json @request.json \\\n  \"https://veil.rip/v1/identities/1/statements\""}],"live":false,"bodyFields":[{"name":"statement","description":"{link, retracts, text, time, title, type: \"statement\", v: 1}: a title of 1 to 200 characters, a text of 1 to 4 000, an https link or null, and the id of the statement it retracts or null.","in":"body","required":true},{"name":"salt","description":"32 random bytes in 64 lowercase hex characters, chosen by the browser.","in":"body","required":true},{"name":"entry","description":"{text, signature}: the log entry {c, key, kind, signer, time, token, v: 2} in canonical JSON without its signature, and the signature of \"veil-rubrica:card-entry:v2\\n\" + text with X-Public-Key.","in":"body","required":true}],"responses":[{"status":201,"contentType":"application/json","description":"The statement, its entry and its proof.","fields":[{"name":"id","type":"integer","description":"The entry’s position in the log: the statement’s permanent id."},{"name":"kind","type":"string","description":"statement or alert."},{"name":"n","type":"integer","description":"The card the statement is by."},{"name":"signer","type":"object","description":"n and token of the person card whose key signed: the card itself, or an owner or admin of an organisation."},{"name":"statement","type":"string","description":"The statement as signed, canonical JSON. Its fields are repeated below for display."},{"name":"salt","type":"string","description":"32 bytes in hex: c of the entry is SHA-256(salt ‖ statement)."},{"name":"entry","type":"string","description":"The log entry with its signature, as the log holds it."},{"name":"i","type":"integer","description":"The entry’s position in the log (the same as id)."},{"name":"publishedAt","type":"string | null","description":"The earliest cosignature on the first checkpoint holding the entry."},{"name":"checkpoint","type":"object | null","description":"size and note of the cosigned checkpoint that dates the entry. Null when the log could not be read."},{"name":"inclusion","type":"array<string> | null","description":"The entry’s RFC 6962 inclusion proof in that checkpoint, hex, deepest first."},{"name":"title","type":"string","description":"The title, 1 to 200 characters."},{"name":"text","type":"string","description":"The text, 1 to 4 000 characters, line breaks kept."},{"name":"link","type":"string | null","description":"An optional https link."},{"name":"retracts","type":"integer | null","description":"The id of the statement this one retracts."},{"name":"retractedBy","type":"object | null","description":"id, publishedAt and title of the statement that retracts this one."},{"name":"url","type":"string","description":"The statement’s page."}]}],"responseObjects":[],"errorFormat":"json","responseHeaders":[{"name":"Content-Type","value":"application/json","description":"Use the declared media type before choosing a parser."},{"name":"Date","value":"HTTP date","description":"Server time. Compare it with your clock when signing requests."},{"name":"Cache-Control","value":"no-store","description":"Ask again each time: a new statement or alert shows at once."}],"errorDetails":[{"status":400,"code":"invalid_json","description":"The body is not a JSON object, contains duplicate keys or has trailing data.","action":"Send one valid JSON object with `Content-Type` application/json.","scope":"common"},{"status":400,"code":"invalid_nonce","description":"The signed-request nonce has an invalid format.","action":"Generate a fresh 16 to 128-character nonce using letters, digits, _ or -.","scope":"common"},{"status":400,"code":"invalid_parameter","description":"A path, query or body field is missing, malformed or outside its accepted range.","action":"Correct the field named in `error.message` before sending again.","scope":"endpoint"},{"status":400,"code":"invalid_signature_alg","description":"`X-Signature-Alg` is unsupported.","action":"Use a supported signing algorithm and matching key/signature encoding.","scope":"common"},{"status":400,"code":"invalid_timestamp","description":"The signed-request timestamp is not Unix milliseconds.","action":"Use an integer Unix timestamp in milliseconds and sign the corrected request.","scope":"common"},{"status":400,"code":"wrong_format","description":"The request or the read belongs to the other log format than this server’s: a v1 record on a format 2 log, or a format 2 read on a format 1 log.","action":"Read `format` in GET /v1/log (absent means 1) and send the shape of that format.","scope":"endpoint"},{"status":401,"code":"invalid_signature","description":"The HTTP request signature does not verify.","action":"Sign the exact method, path and body bytes, without changing them afterwards.","scope":"common"},{"status":401,"code":"missing_signature","description":"The request requires a device signature and its headers are missing or unreadable.","action":"Send `X-Public-Key`, `X-Signature` and the signed-request metadata. An API key does not replace a device signature.","scope":"common"},{"status":401,"code":"stale_timestamp","description":"The signed timestamp is outside the accepted clock window.","action":"Compare the server Date header with your clock, then generate a fresh nonce, timestamp and signature.","scope":"common"},{"status":403,"code":"identity_banned","description":"The acting identity is suspended by Veil staff.","action":"Wait for the restriction to end or ask staff to review it.","scope":"endpoint"},{"status":403,"code":"key_banned","description":"The device signing key is suspended by Veil staff.","action":"Use an authorised active key or ask staff to review the restriction.","scope":"endpoint"},{"status":403,"code":"key_mismatch","description":"The signing key does not have authority over this card or operation.","action":"Use an active device key with the required owner or member role.","scope":"endpoint"},{"status":403,"code":"role_not_allowed","description":"The signing role is not allowed to make this change.","action":"Use a key with the required role.","scope":"endpoint"},{"status":404,"code":"not_found","description":"The requested card, key, alias, code or resource was not found.","action":"Check the identifier. Temporary codes and previews may already have expired or been used.","scope":"endpoint"},{"status":409,"code":"already_retracted","description":"The statement named in `retracts` is already retracted.","action":"Publish a new statement instead.","scope":"endpoint"},{"status":409,"code":"erase_pending","description":"On a format 2 log, the card has an erasure waiting. Its signed closing names the current version, so the card cannot publish until the erasure is cancelled.","action":"Cancel the erasure with POST /v1/identities/{n}/erase/cancel, then publish the version.","scope":"endpoint"},{"status":409,"code":"replayed_nonce","description":"The signing key already used this nonce.","action":"Check whether the previous request succeeded before making a fresh signed request.","scope":"common"},{"status":409,"code":"signer_stale","description":"On a format 2 log, `signer.n` of a signed entry is behind the current version of the card that signs.","action":"Read the signing card again and sign the entry with its current version in `signer.n`.","scope":"endpoint"},{"status":410,"code":"erased","description":"The card existed and was erased or removed. The response also carries its public erasure receipt.","action":"Stop using this card as current authority. Retain only the public receipt if needed.","scope":"endpoint"},{"status":413,"code":"payload_too_large","description":"The request body exceeds this endpoint’s byte limit.","action":"Reduce the encoded body. API-key management accepts at most 8 KiB.","scope":"common"},{"status":422,"code":"bad_entry","description":"On a format 2 log, the `salt`, `entry` or `closing` of a request is wrong. `error.message` names the field.","action":"Correct the named field, sign the entry again with the request key and send a new signed request.","scope":"endpoint"},{"status":422,"code":"invalid_statement","description":"A card’s alert or statement does not follow its schema: a missing or extra field, a text too long or with hidden characters, a link that is not https, a date in the future.","action":"Fix the field named in `error.message` and sign the statement and its entry again.","scope":"endpoint"},{"status":429,"code":"rate_limited","description":"An anonymous, signed-request or API-key allowance was exceeded.","action":"Wait at least `Retry-After` seconds. Share a queue and retry budget across your workers.","scope":"common"},{"status":429,"code":"statement_quota","description":"The card published 10 statements in the last 24 hours, retractions included.","action":"Wait the seconds that `Retry-After` gives.","scope":"endpoint"},{"status":500,"code":"internal_error","description":"An unexpected server error occurred. Internal details are not returned.","action":"Retry reads with bounded backoff. Check write results before sending a new signed operation.","scope":"common"},{"status":503,"code":"busy","description":"The application has reached a temporary concurrency or database limit.","action":"Wait `Retry-After` seconds and retry with bounded backoff.","scope":"common"},{"status":503,"code":"log_unavailable","description":"The public log cannot currently be read, verified or used for publication.","action":"For reads, keep the result unverified and try later. Before retrying a write, read the card to check whether publication completed.","scope":"endpoint"}],"documentationUrl":"https://veil.rip/docs/publish-statement","supportsBrowserFetch":false,"browserMode":"local-or-server"}}