Responses and errors
Read HTTP statuses, response fields and stable error codes, then decide whether to correct, retry or stop.
Directory API errors use an error object with code and message. Branch on the HTTP status and error.code. The English message explains the current failure and can change; do not parse it as a contract.
{
"error": {
"code": "invalid_parameter",
"message": "Supply exactly one lookup parameter"
}
}A successful request can return a negative verdict#
HTTP 200 means the verification request was processed. A false valid field is a verification result. Read reason and signatureOk before accepting the message. Likewise, an alias availability response can return 200 with available=false, and a full card can have proof=null when current log verification is unavailable.
Correcting and retrying requests#
- 400, 413 and 422 need a corrected input. Sending identical bytes again does not fix them.
- 401 and 403 need a valid credential, signature, scope or role. An invalid API key never falls back to anonymous access.
- 409 needs a fresh read of the current state. Check whether the earlier write already succeeded before signing another operation.
- For
rate_limitedand busy, waitRetry-Afterseconds. Use bounded backoff with jitter for transient read failures. Never retry a signed write blindly; read the card or operation state first, then use a fresh nonce and signature if needed.
Erased cards#
HTTP 410 with error.code erased also returns erased=true, n, closedAt, token and logIndexes. A staff removal additionally returns removed=true, category and, when available, logIndex. This receipt is historical; it does not restore the card’s authority. Temporary proof_expired also uses 410 and has only the ordinary error envelope.
Media types and empty responses#
Read Content-Type before parsing. Images, scripts, signed log notes and tiles are not JSON. A conditional read of the documentation or of a card can return 304 with no body. Reuse the representation saved with its ETag. The OAuth callback renders an HTML return page even when it fails. Raw resources on log.veil.rip use their own text or binary protocol and do not promise the directory error envelope. A proxy or CDN can also return an HTML error, so handle JSON parsing failures.
Error codes#
| HTTP | Code | Meaning and next step |
|---|---|---|
| 400 | bad_request | The HTTP request could not be read. Check its encoding and body. |
| 400 | hash_needed | The signature covers a hash of the file that the request does not carry. The message begins with its name: sha256, sha512 or blake2b512. Compute that hash of the file (sha256sum, sha512sum or b2sum) and send it in the field of the same name. |
| 400 | invalid_alias | The alias does not meet the ASCII name rules. Use 3 to 32 lowercase ASCII letters, digits, hyphens or underscores; start with a letter and end with a letter or digit. |
| 400 | invalid_block | The signature block cannot be parsed or names another directory. Paste the complete block with its message and use the directory it names. |
| 400 | invalid_file_signature | The signature of a file or a checksum list can’t be read. The message begins with the reason: unreadable, namespace (an SSH signature made for another use than files), pgp_type, pgp_version, weak_hash, list_unreadable or block_message. Send the signature file as the tool wrote it. Sign files with ssh-keygen -Y sign -n file, gpg --detach-sign with SHA-256 or SHA-512, or minisign -Sm. |
| 400 | invalid_json | The body is not a JSON object, contains duplicate keys or has trailing data. Send one valid JSON object with |
| 400 | invalid_name | A name contains invisible, direction-control or unsupported format characters. Use a visible name in the accepted Unicode form and sign the corrected request. |
| 400 | invalid_nonce | The signed-request nonce has an invalid format. Generate a fresh 16 to 128-character nonce using letters, digits, _ or -. |
| 400 | invalid_parameter | A path, query or body field is missing, malformed or outside its accepted range. Correct the field named in |
| 400 | invalid_signature_alg |
Use a supported signing algorithm and matching key/signature encoding. |
| 400 | invalid_timestamp | The signed-request timestamp is not Unix milliseconds. Use an integer Unix timestamp in milliseconds and sign the corrected request. |
| 400 | wrong_format | 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. Read |
| 401 | invalid_api_key | The bearer credential is malformed, unknown, expired or revoked. Use an active key. To use anonymous public access, remove |
| 401 | invalid_signature | The HTTP request signature does not verify. Sign the exact method, path and body bytes, without changing them afterwards. |
| 401 | missing_signature | The request requires a device signature and its headers are missing or unreadable. Send |
| 401 | stale_timestamp | The signed timestamp is outside the accepted clock window. Compare the server Date header with your clock, then generate a fresh nonce, timestamp and signature. |
| 403 | api_key_scope | The API key lacks read or verify scope, or the endpoint does not accept bearer keys. Use the correct scope. Identity writes and key management still require a device signature. |
| 403 | api_key_suspended | This API key is suspended. Ask staff to restore it; another key cannot lift an account suspension. |
| 403 | cors_not_allowed | A browser preflight requests a method or header outside public CORS. Use anonymous public reads or verification from the browser. Keep bearer and device-signing credentials in their supported local/server flow. |
| 403 | developer_suspended | The owner’s API account is suspended or the owner card is unavailable. Check the owner account and any staff restriction. Creating another key does not bypass suspension. |
| 403 | identity_banned | The acting identity is suspended by Veil staff. Wait for the restriction to end or ask staff to review it. |
| 403 | invalid_token | The stop link’s token is not the server’s signature for this card. Copy the whole link from the email, or opt out from the account page. |
| 403 | key_banned | The device signing key is suspended by Veil staff. Use an authorised active key or ask staff to review the restriction. |
| 403 | key_mismatch | The signing key does not have authority over this card or operation. Use an active device key with the required owner or member role. |
| 403 | recovery_key_only | A recovery-only operation was signed with a different role. Use the recovery key in the documented recovery flow. |
| 403 | role_not_allowed | The signing role is not allowed to make this change. Use a key with the required role. |
| 403 | signer_not_authorised | The organisation member or signing role cannot publish this change. Check accepted membership and the role required for the change. |
| 404 | not_found | The requested card, key, alias, code or resource was not found. Check the identifier. Temporary codes and previews may already have expired or been used. |
| 409 | account_locked | Moderation prevents this account from being claimed. Check the current card’s public moderation state. |
| 409 | action_pending | An earlier alert about the same account, or a staff action of the same family, is still waiting for the log. Try again once it is published, within a few minutes. |
| 409 | alert_in_force | The account already carries this same alert. Nothing to do. To correct the date or the new account, send the alert with the new details. |
| 409 | alert_not_in_force |
Read the card’s alerts (GET /v1/identities/{n}/alerts) before ending one. |
| 409 | alias_quarantined | The alias is in its 30-day release quarantine. Inspect |
| 409 | alias_reserved | The chosen alias is reserved for the service. Choose another alias. |
| 409 | alias_taken | The alias is claimed or a publication has reserved it. Choose another alias or check availability again after the publication finishes. |
| 409 | already_member | This membership already exists. Read current memberships before publishing another invitation. |
| 409 | already_retracted | The statement named in Publish a new statement instead. |
| 409 | api_key_limit | The owner already has 20 unexpired, unrevoked API keys. Revoke an unused key before creating another. Suspended keys still count. |
| 409 | callback_used | This OAuth sign-in already returned once. Resume the original browser flow or start a fresh linking session. |
| 409 | erase_already_requested | An erasure is already waiting. Read its scheduled execution time from the card. |
| 409 | erase_pending | 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. Cancel the erasure with POST /v1/identities/{n}/erase/cancel, then publish the version. |
| 409 | grant_used | A verification or membership grant has already been consumed. Check the resulting card version, then obtain a fresh grant if still needed. |
| 409 | identity_exists | An identity already uses this token. Check the existing publication. Generate a new identity only when a different card is intended. |
| 409 | invitation_closed | The invitation was accepted, declined or otherwise closed. Read current memberships before continuing. |
| 409 | invitation_withdrawn | The organisation no longer offers the invitation. Ask for a new invitation. |
| 409 | key_already_used | A device key already belongs to an identity and cannot be reused by another. Use the existing card or generate a fresh device key. |
| 409 | no_erase_requested | There is no pending erasure to cancel. Read the card’s current state. |
| 409 | proof_pending | The OAuth sign-in has not returned yet. Finish the provider sign-in in the browser that started it. |
| 409 | replayed_nonce | The signing key already used this nonce. Check whether the previous request succeeded before making a fresh signed request. |
| 409 | report_already_open | The caller already has an open report for this target. Wait for staff to resolve the existing report. |
| 409 | salt_ticket_invalid | The salt ticket is unknown, expired or already consumed. Request a fresh ticket and rebuild and sign version one with it. |
| 409 | signer_stale | On a format 2 log, Read the signing card again and sign the entry with its current version in |
| 409 | takeover_already_open | A takeover for this linked account is already open. Read the pending takeover instead of opening another. |
| 409 | takeover_closed | The takeover is already cancelled or completed. Read its current status. |
| 409 | version_conflict | The version cannot be published against the card’s current state. Read the latest card, reconcile the intended change and sign a new version. |
| 410 | erased | The card existed and was erased or removed. The response also carries its public erasure receipt. Stop using this card as current authority. Retain only the public receipt if needed. |
| 410 | proof_expired | The temporary account-proof session has expired. Start a new linking session. |
| 413 | payload_too_large | The request body exceeds this endpoint’s byte limit. Reduce the encoded body. API-key management accepts at most 8 KiB. |
| 422 | account_not_linked | The alert is about an account the card does not hold, or Name a linked account, or leave |
| 422 | authority_limit | On a format 2 log, the version would leave more than 1,024 members in an organisation’s authority: its owners and admins, or every staff role in the Veil organisation. Remove an owner or admin first. A removal in the same version also frees a place. |
| 422 | bad_entry | On a format 2 log, the Correct the named field, sign the entry again with the request key and send a new signed request. |
| 422 | cannot_revoke_self | Self-revocation would leave no active signing key, or a recovery key is being revoked without its replacement. Keep another active key. Replace a recovery key in the same version that revokes it. |
| 422 | closing_required | On a format 2 log, an erasure arrived without its signed closing entry in Sign the closing entry with the same key and send it as |
| 422 | external_limit | The card would hold more than 16 public keys from elsewhere, or one addExternalKey operation is larger than 16 KiB in canonical JSON. Remove a key first. A removal in the same version also frees a place. |
| 422 | file_needed | An OpenPGP detached signature or a legacy minisign signature of the file itself. It covers the file’s bytes, and the server never sees the file. Check it where the file is: on veil.rip/verify in a browser or with the GitHub action. Or sign a checksum list, which the server checks in every format. |
| 422 | grant_expired | The verification or membership grant expired. Obtain a fresh grant before publishing the version. |
| 422 | invalid_grant | The verification or membership grant is invalid or belongs to another operation. Use the grant issued for this exact card, account or membership. |
| 422 | invalid_logo | The image fails type, size, dimensions or content validation. Since 27 October 2026 also Correct the image according to |
| 422 | invalid_op | An operation is unknown, malformed, not one of this card’s type or version, or changes nothing. Correct the operation named in |
| 422 | invalid_record | The identity version violates the signed-record rules. Correct the validation issue, canonicalise the record and sign it again. |
| 422 | invalid_record_signature | The identity-version signature does not verify. Sign the exact canonical record with its identity-version prefix and declared key. |
| 422 | invalid_statement | 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. Fix the field named in |
| 422 | invalid_statement_signature | The signed erasure statement does not verify. Sign the exact statement bytes with the required prefix and key. |
| 422 | key_expired | The OpenPGP key or its signing subkey has expired. Extend its expiry on your machine (gpg --quick-set-expire), then paste the key again and sign a new challenge. |
| 422 | key_extra_packets | The OpenPGP key still carries a photo, another person’s certification, or other material a card does not keep. 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. |
| 422 | key_revoked | The OpenPGP key carries a revocation signature. A revoked key can’t go on a card. Bring a key you still use. |
| 422 | key_unsupported | 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. 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. |
| 422 | key_weak | 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). Bring a stronger key. ssh-keygen -Y sign signs RSA with rsa-sha2-512 by default. |
| 422 | keys_limit | On a format 2 log, the version would leave a person with more than 64 active keys. Revoke a key first. A revocation in the same version also frees a place. |
| 422 | label_mismatch | The label’s prev, body, auth or state is not what the chain, the body and the operations give. Read the card again, plan the operations on its current state and sign the new label. |
| 422 | last_owner | The change would leave an organisation without an accepted owner. Add or accept another owner before removing the last one. |
| 422 | links_need_verification | A new account link has no acceptable verification grant. Complete the account proof before publishing its link. |
| 422 | members_limit | On a format 2 log, an invitation would take an organisation past 100,000 members and pending invitations. Remove members or withdraw invitations before inviting again. |
| 422 | membership_needs_grant | An organisation membership lacks its acceptance grant. Accept the invitation and include its grant in the next signed version. |
| 422 | no_verified_email | The card has no verified email link, so there is nowhere to send the notices. Link an email address to the card first, then opt in. |
| 422 | not_a_person | The card is an organisation where a person is required. Use the organisation endpoint or an eligible person’s card. |
| 422 | not_an_organization | The card is a person where an organisation is required. Use the person endpoint or an organisation card. |
| 422 | not_for_organizations | This proof method links a personal account and is unavailable to organisations. Choose a supported organisation proof method from /v1/platforms. |
| 422 | private_key_material | 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. Treat that key as exposed and replace it. Send only the public key and the signature of the challenge. |
| 422 | proof_anchor | 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. Quote the cosigned checkpoint note as the card gives it in index.checkpoint.note, whole. |
| 422 | proof_failed | The provider, code or published proof did not verify. Read |
| 422 | proof_invalid | 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. 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. |
| 422 | proof_not_found | The requested DNS record, website file or profile backlink is not visible yet. Publish the exact challenge and allow for propagation before checking again. |
| 422 | proof_stale | 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. Make a new challenge from the card’s latest checkpoint and sign it again. |
| 422 | self_target | A card cannot report itself. Choose the card the report concerns. |
| 422 | staff_org_target | This operation cannot target Veil’s staff organisation. Choose an eligible target. |
| 429 | alert_quota | The card made 10 account alerts in the last 24 hours. Wait the seconds that |
| 429 | rate_limited | An anonymous, signed-request or API-key allowance was exceeded. Wait at least |
| 429 | statement_quota | The card published 10 statements in the last 24 hours, retractions included. Wait the seconds that |
| 429 | too_many_attempts | This proof session exceeded its wrong-code attempt limit. Start a new proof session. This code is distinct from quota |
| 500 | internal_error | An unexpected server error occurred. Internal details are not returned. Retry reads with bounded backoff. Check write results before sending a new signed operation. |
| 501 | format_not_implemented | Only JSON oEmbed responses are supported. Request format=json. |
| 502 | log_mismatch | The log response did not match the expected signed entry or proof. Do not accept the publication as verified. Check the card and log before retrying. |
| 503 | busy | The application has reached a temporary concurrency or database limit. Wait |
| 503 | log_unavailable | The public log cannot currently be read, verified or used for publication. For reads, keep the result unverified and try later. Before retrying a write, read the card to check whether publication completed. |
| 503 | mail_unavailable | The verification email could not be sent. Try starting the email proof again in a few minutes. |
| 503 | no_mail_transport | This server can’t send email. Nothing to retry: the server has no mail transport. |
| 503 | provider_not_configured | The selected proof provider is unavailable on this server. Read /v1/platforms for available methods. |
- Keep requests bounded and follow the response when a call needs to wait.
- Give a server integration a shared allowance without granting authority over a Veil card.
- Use an authorised device key for changes to cards and organisations.
- Verify a messageCheck a signature block or an explicit key, message and signature. This POST only reads.
POST /v1/verify - Read one documentation entryRead one guide or endpoint contract, including examples.
GET /v1/integrations/{id}