Skip to content

Errors

Every error response from the API is JSON with an English error message and a stable machine-readable code:

{
"error": "A group with this name already exists",
"code": "group_name_taken"
}

The error text is meant for logs and scripts. It can change between releases and is not translated. Use code to decide what to do or what to show a user.

Some errors also carry a params object with the values needed to build a localized message, for example { "resource": "people", "limit": 50 } on plan_limit_reached. The keys depend on the code and are listed in the table below.

Some 200 validation responses also carry a code. For example, POST /api/user/import/validate returns 200 with code: "plan_limit_reached" and params next to its existing fields when the import would exceed the plan.

New codes are added over time. If a client meets a code it does not know, it should fall back to the generic code for the HTTP status: 400 is bad_request, 401 is unauthorized, 403 is forbidden, 404 is not_found, 409 is conflict, 413 is payload_too_large, 429 is rate_limited, 500 is internal_error and 503 is service_unavailable. Any other 4xx falls back to bad_request and any other 5xx (such as the 502 of carddav_unreachable) to internal_error.

A code is never renamed, removed or reused once it has shipped, and its HTTP status does not change. From this version on, existing response fields such as error and details keep their shape, and new information arrives as new codes or new params keys. The release that introduced codes moved a few top-level fields into params (retryAfter on the password reset and verification email cooldowns, path on unknown API routes, available and requested on import limits) and replaced the import code LIMIT_EXCEEDED with plan_limit_reached.

A validation_failed response keeps the legacy details array (field and message) and adds a fields array that clients can localize:

{
"error": "Validation failed",
"code": "validation_failed",
"details": [{ "field": "phones.0.number", "message": "Too big" }],
"fields": [
{ "path": "phones.0.number", "issue": "too_long", "params": { "max": 50 } }
]
}

Each entry has a path (dot-separated, array indexes as numbers), an issue and optional numeric params. The issue is one of:

IssueMeaning
requiredThe field is missing. An empty string is reported as too_short with params.min.
too_longThe value is longer than allowed. params.max gives the limit.
too_shortThe value is shorter than required. params.min gives the limit.
invalid_formatThe value does not have the expected format, such as an email address or URL.
invalid_valueThe value is not acceptable for any other reason.
CodeStatusDescriptionParams
bad_request400The request could not be processed.
unauthorized401Authentication is missing or invalid.
forbidden403The caller may not perform this action.
not_found404The resource does not exist or is not visible to the caller.path (unknown API routes only)
conflict409The request conflicts with the current state.
payload_too_large413The request body is too large.
rate_limited429Too many requests. Retry after the Retry-After header.retryAfter (password reset and verification email cooldowns)
internal_error500Unexpected server error.
service_unavailable503A dependency such as the database is unavailable.
validation_failed400The body failed validation. See fields.
invalid_json400The body is not valid JSON.
invalid_origin403The request origin is not allowed.
invalid_query400A query parameter is invalid.param
sync_cursor_expired410The sync cursor is older than retained deletions. Discard the local cache and sync again from cursor 0.
invalid_credentials401Wrong email or password.
account_locked403Too many failed sign-ins; the account is temporarily locked.
email_not_verified403The email address has not been verified.
password_login_disabled403Password sign-in is disabled on this server.
session_expired401The device session expired or was revoked.
invalid_token401The bearer token is unknown or malformed.
access_token_expired401The access token expired. Refresh it.
invalid_code401The sign-in code is unknown, expired or the verifier does not match.
interactive_session_required403This action needs a browser or device session, not an API token.
not_a_device_session400The caller is not a device session.
plan_limit_reached403The plan limit for a resource is reached.resource, limit; imports also send available, requested
subscription_required403An active paid subscription is required.
group_name_taken409A group with this name already exists.
relationship_type_name_taken409A relationship type with this name already exists.name
custom_field_name_taken409A custom field with this name already exists.
important_date_type_exists409The person already has a date of this type.type
relationship_exists409This relationship already exists.
email_in_use409The email address is already used by another account.
already_group_member409The person is already in this group.
self_relationship400A person cannot be related to, or dismissed with, themselves.
relationship_type_required400A relationship type is required.
invalid_person_ids400One or more person IDs are invalid.
name_needs_alphanumeric400The name must contain at least one letter or digit.
reorder_incomplete400A reorder must include every active item exactly once.
invalid_confirmation400The confirmation text does not match.
wrong_password400The password is incorrect.
password_required400A password is required.
oauth_account_no_password400The account signs in with a provider and has no password.
photo_missing400No photo file was provided.
photo_too_large413The photo is too large.maxBytes
unsupported_file_type400The file type is not supported here.
invalid_vcard400The vCard file could not be parsed.
feature_disabled403The feature is turned off.feature, scope
conflict_already_resolved409The sync conflict was already resolved.
carddav_auth_failed400The CardDAV server rejected the credentials.
carddav_unreachable502The CardDAV server could not be reached or failed.
invalid400Notification endpoint: the request or URL is invalid.
duplicate409Notification endpoint: that destination is already added.
dns400Notification endpoint: the host name did not resolve.
policy400Notification endpoint: the address is not allowed.
blocked400Notification endpoint: the address is blocked.
credentials_in_url400Notification endpoint: the URL contains a username or password.
not_ntfy400Notification endpoint: the server is not an ntfy server.
too_long400Notification endpoint: the URL is too long.
endpoint_limit_reached409Notification endpoint: the per-user limit is reached.limit