{"revision":"0ed62d2c742f8e20","language":"en","entry":{"id":"publish-person","kind":"endpoint","group":"manage","title":"Publish a person’s next version","method":"POST","path":"/v1/identities/{n}/versions","paths":[],"summary":"Change keys, linked accounts, alias or other signed card fields.","response":"201 with n, id, url, token, version, commitment, logIndex, publishedAt and checkpoint. The response follows witnessed publication.","parameters":[{"name":"n","description":"Permanent positive card number.","in":"path","required":true}],"sample":"/v1/identities/1/versions","access":"signed","base":"directory","notes":["On a log in format 2 the body is {label, body, signature, pictureData?}, with clientSalt and saltTicket required for version 1; a record is refused with wrong_format.","From 29 October 2026 a person’s body can carry addExternalKey {type, pub, fp, label, proof: {anchor, at, by, sig, variant?}} and removeExternalKey {fp}: public keys from elsewhere with their proof of possession. proof.variant names the form of the challenge that was signed when it isn’t the bytes as offered (crlf, bom, noeol and their combinations). The challenge and the checks are in the guide to public keys from elsewhere (external-keys-file) and in docs/formato-v2.md, section 4."],"errors":["400 invalid_alias","400 invalid_json","400 invalid_name","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 recovery_key_only","403 role_not_allowed","403 signer_not_authorised","404 not_found","409 account_locked","409 alias_quarantined","409 alias_reserved","409 alias_taken","409 already_member","409 erase_pending","409 grant_used","409 invitation_withdrawn","409 key_already_used","409 replayed_nonce","409 takeover_already_open","409 version_conflict","410 erased","413 payload_too_large","422 cannot_revoke_self","422 external_limit","422 grant_expired","422 invalid_grant","422 invalid_logo","422 invalid_op","422 invalid_record","422 invalid_record_signature","422 key_expired","422 key_extra_packets","422 key_revoked","422 key_unsupported","422 key_weak","422 keys_limit","422 label_mismatch","422 last_owner","422 links_need_verification","422 membership_needs_grant","422 not_a_person","422 private_key_material","422 proof_anchor","422 proof_invalid","422 proof_stale","429 rate_limited","500 internal_error","502 log_mismatch","503 busy","503 log_unavailable"],"related":["signed-requests","external-keys-file"],"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/versions\""}],"bodyFields":[{"name":"record","description":"The complete canonical identity or organisation record, including its history fields.","in":"body","required":true},{"name":"signature","description":"The record signature, separate from the request signature header.","in":"body","required":true},{"name":"logoData","description":"The image that record.logo names by hash, in standard padded base64 on one line. Required when the version brings a new image. With an unchanged image it can be left out, and if sent it must match.","in":"body","required":false},{"name":"nonce","description":"A fresh 16 to 128 character nonce using letters, digits, underscore or hyphen.","in":"body","required":true},{"name":"timestamp","description":"Unix milliseconds within two minutes of the server clock.","in":"body","required":true},{"name":"label","description":"Format 2: the label as a JSON object; the server encodes it canonically and checks the signature on those bytes.","in":"body","required":false},{"name":"body","description":"Format 2: the body as a JSON object, its hash in label.body.","in":"body","required":false},{"name":"pictureData","description":"Format 2: the bytes of a new picture the body names by hash, in standard padded base64.","in":"body","required":false}],"live":false,"responses":[{"status":201,"contentType":"application/json","description":"201 with n, id, url, token, version, commitment, logIndex, publishedAt and checkpoint. The response follows witnessed publication.","fields":[{"name":"n","type":"integer","description":"Permanent card number."},{"name":"id","type":"string","description":"Version-one commitment in hex."},{"name":"url","type":"string","description":"Permanent card URL."},{"name":"token","type":"string","description":"Stable log pseudonym in base64url."},{"name":"version","type":"integer","description":"Published version number."},{"name":"commitment","type":"string","description":"This version’s commitment in hex."},{"name":"logIndex","type":"integer","description":"Zero-based log index."},{"name":"publishedAt","type":"string","description":"Earliest cosignature time at publication."},{"name":"checkpoint","type":"Checkpoint","description":"First checkpoint covering the version."}]}],"responseObjects":[{"id":"checkpoint","title":"Checkpoint","fields":[{"name":"size","type":"integer","description":"Number of leaves in the signed tree."},{"name":"root","type":"string","description":"Root hash in base64."},{"name":"note","type":"string","description":"Full signed note, including log and witness signatures."}],"typeName":"Checkpoint"}],"errorFormat":"json","errorDetails":[{"status":400,"code":"invalid_alias","description":"The alias does not meet the ASCII name rules.","action":"Use 3 to 32 lowercase ASCII letters, digits, hyphens or underscores; start with a letter and end with a letter or digit.","scope":"endpoint"},{"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":"endpoint"},{"status":400,"code":"invalid_name","description":"A name contains invisible, direction-control or unsupported format characters.","action":"Use a visible name in the accepted Unicode form and sign the corrected request.","scope":"endpoint"},{"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":"common"},{"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":"recovery_key_only","description":"A recovery-only operation was signed with a different role.","action":"Use the recovery key in the documented recovery flow.","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":403,"code":"signer_not_authorised","description":"The organisation member or signing role cannot publish this change.","action":"Check accepted membership and the role required for the change.","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":"account_locked","description":"Moderation prevents this account from being claimed.","action":"Check the current card’s public moderation state.","scope":"endpoint"},{"status":409,"code":"alias_quarantined","description":"The alias is in its 30-day release quarantine.","action":"Inspect `reusableAt` and the previous holder through the alias endpoint.","scope":"endpoint"},{"status":409,"code":"alias_reserved","description":"The chosen alias is reserved for the service.","action":"Choose another alias.","scope":"endpoint"},{"status":409,"code":"alias_taken","description":"The alias is claimed or a publication has reserved it.","action":"Choose another alias or check availability again after the publication finishes.","scope":"endpoint"},{"status":409,"code":"already_member","description":"This membership already exists.","action":"Read current memberships before publishing another invitation.","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":"grant_used","description":"A verification or membership grant has already been consumed.","action":"Check the resulting card version, then obtain a fresh grant if still needed.","scope":"endpoint"},{"status":409,"code":"invitation_withdrawn","description":"The organisation no longer offers the invitation.","action":"Ask for a new invitation.","scope":"endpoint"},{"status":409,"code":"key_already_used","description":"A device key already belongs to an identity and cannot be reused by another.","action":"Use the existing card or generate a fresh device key.","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":"takeover_already_open","description":"A takeover for this linked account is already open.","action":"Read the pending takeover instead of opening another.","scope":"endpoint"},{"status":409,"code":"version_conflict","description":"The version cannot be published against the card’s current state.","action":"Read the latest card, reconcile the intended change and sign a new version.","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":"endpoint"},{"status":422,"code":"cannot_revoke_self","description":"Self-revocation would leave no active signing key, or a recovery key is being revoked without its replacement.","action":"Keep another active key. Replace a recovery key in the same version that revokes it.","scope":"endpoint"},{"status":422,"code":"external_limit","description":"The card would hold more than 16 public keys from elsewhere, or one addExternalKey operation is larger than 16 KiB in canonical JSON.","action":"Remove a key first. A removal in the same version also frees a place.","scope":"endpoint"},{"status":422,"code":"grant_expired","description":"The verification or membership grant expired.","action":"Obtain a fresh grant before publishing the version.","scope":"endpoint"},{"status":422,"code":"invalid_grant","description":"The verification or membership grant is invalid or belongs to another operation.","action":"Use the grant issued for this exact card, account or membership.","scope":"endpoint"},{"status":422,"code":"invalid_logo","description":"The image fails type, size, dimensions or content validation.","action":"Correct the image according to `error.message` before uploading again.","scope":"endpoint"},{"status":422,"code":"invalid_op","description":"An operation is unknown, malformed, not one of this card’s type or version, or changes nothing.","action":"Correct the operation named in `error.message`.","scope":"endpoint"},{"status":422,"code":"invalid_record","description":"The identity version violates the signed-record rules.","action":"Correct the validation issue, canonicalise the record and sign it again.","scope":"endpoint"},{"status":422,"code":"invalid_record_signature","description":"The identity-version signature does not verify.","action":"Sign the exact canonical record with its identity-version prefix and declared key.","scope":"endpoint"},{"status":422,"code":"key_expired","description":"The OpenPGP key or its signing subkey has expired.","action":"Extend its expiry on your machine (gpg --quick-set-expire), then paste the key again and sign a new challenge.","scope":"endpoint"},{"status":422,"code":"key_extra_packets","description":"The OpenPGP key still carries a photo, another person’s certification, or other material a card does not keep.","action":"Add the key from your card’s page, which keeps only what a card holds: the key, its signing subkeys and the user IDs you choose.","scope":"endpoint"},{"status":422,"code":"key_revoked","description":"The OpenPGP key carries a revocation signature.","action":"A revoked key can’t go on a card. Bring a key you still use.","scope":"endpoint"},{"status":422,"code":"key_unsupported","description":"A key type Veil does not hold: SSH keys on a security key (sk-…), ECDSA P-521, SSH certificates, an OpenPGP key other than version 4, or an OpenPGP algorithm other than RSA, ECDSA P-256 or P-384 and Ed25519.","action":"Use one of the listed types: ssh-ed25519, ssh-ecdsa-p256, ssh-ecdsa-p384, ssh-rsa, pgp, minisign, ed25519, ml-dsa-44, ml-dsa-65, ml-dsa-87.","scope":"endpoint"},{"status":422,"code":"key_weak","description":"RSA under 2048 bits or DSA, in SSH or in OpenPGP, an Ed25519 point of small order, or an RSA signature made with SHA-1 (ssh-rsa).","action":"Bring a stronger key. ssh-keygen -Y sign signs RSA with rsa-sha2-512 by default.","scope":"endpoint"},{"status":422,"code":"keys_limit","description":"On a format 2 log, the version would leave a person with more than 64 active keys.","action":"Revoke a key first. A revocation in the same version also frees a place.","scope":"endpoint"},{"status":422,"code":"label_mismatch","description":"The label’s prev, body, auth or state is not what the chain, the body and the operations give.","action":"Read the card again, plan the operations on its current state and sign the new label.","scope":"endpoint"},{"status":422,"code":"last_owner","description":"The change would leave an organisation without an accepted owner.","action":"Add or accept another owner before removing the last one.","scope":"endpoint"},{"status":422,"code":"links_need_verification","description":"A new account link has no acceptable verification grant.","action":"Complete the account proof before publishing its link.","scope":"endpoint"},{"status":422,"code":"membership_needs_grant","description":"An organisation membership lacks its acceptance grant.","action":"Accept the invitation and include its grant in the next signed version.","scope":"endpoint"},{"status":422,"code":"not_a_person","description":"The card is an organisation where a person is required.","action":"Use the organisation endpoint or an eligible person’s card.","scope":"endpoint"},{"status":422,"code":"private_key_material","description":"A text in an addExternalKey operation holds private key material: a PEM or OpenSSH private key, a PGP private block or secret key packet, a minisign secret key, a 64-byte Ed25519 secret key, or an ML-DSA seed or secret key. Nothing is stored and the answer never repeats the text.","action":"Treat that key as exposed and replace it. Send only the public key and the signature of the challenge.","scope":"endpoint"},{"status":422,"code":"proof_anchor","description":"The quoted checkpoint is not from this log, lacks the log’s signature or the quorum of cosignatures, or its root is not the log’s root at that size.","action":"Quote the cosigned checkpoint note as the card gives it in index.checkpoint.note, whole.","scope":"endpoint"},{"status":422,"code":"proof_invalid","description":"The challenge names another device key than the one signing the version, or the signature is not the key’s signature of the challenge for this card. The message names the cause: another key, another SSH namespace, a hash that doesn’t count, the challenge saved as UTF-16 or with an empty line after it, other text, or a form other than the one proof.variant names.","action":"Sign the exact challenge bytes again from the browser that publishes the version: with namespace veil for SSH, as a detached or clear-signed signature of the file for OpenPGP. A copy saved on Windows (CRLF, a UTF-8 BOM, no final line feed) counts when proof.variant names its form.","scope":"endpoint"},{"status":422,"code":"proof_stale","description":"The checkpoint quoted by the challenge was cosigned more than 7 days ago, or the stated time at is older than 7 days or ahead of the server’s clock.","action":"Make a new challenge from the card’s latest checkpoint and sign it 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":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":502,"code":"log_mismatch","description":"The log response did not match the expected signed entry or proof.","action":"Do not accept the publication as verified. Check the card and log before retrying.","scope":"endpoint"},{"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"}],"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":"Location","value":"relative URL","description":"Published card/proof address, or permanent widget redirect target."},{"name":"Retry-After","value":"integer seconds, on rate_limited or busy","description":"Wait at least this long before retrying. Other 429/503 codes do not necessarily include it."}],"documentationUrl":"https://veil.rip/docs/publish-person","supportsBrowserFetch":false,"browserMode":"local-or-server"}}