Skip to content

Error taxonomy

Every error response has the same shape (see common/errors.py):

{
  "errors": [
    { "code": "auth.invalid_credentials", "description": "Invalid credentials." }
  ]
}

Codes are namespaced by domain (auth., account., ...) and this list grows as new missions add their own — this is the first real taxonomy, seeded by the Accounts & Authentication mission.

Generic (pre-existing scaffold)

Code HTTP status Meaning
http_{status_code} matches status Framework-native HTTPException with no domain-specific code yet.
validation_error 422 Request body/query failed Pydantic validation.
internal_server_error 500 Unhandled exception — logged server-side, no internal detail leaked.

Auth / Accounts (this mission)

Code HTTP status Meaning
auth.invalid_credentials 401 Wrong email/password, revoked/expired API key, or malformed/invalid bearer token. Deliberately generic — see enumeration note below.
auth.account_not_active 403 Account exists and credentials are correct, but its status isn't active (e.g. still pending_activation).
auth.token_expired 400 Activation or password-reset token is invalid, already used, or past its TTL.
auth.rate_limited 429 Sensitive endpoint (register/login/password-reset-request/oauth-token) hit its Redis-backed rate limit. Response includes a Retry-After header.
auth.locked 423 Account temporarily locked after repeated failed login attempts (progressive backoff).
auth.insufficient_scope 403 Bearer token decoded fine but lacks the scope the endpoint requires (e.g. an account:manage token used where accounts:read is needed, or vice versa).
auth.unsupported_grant_type 400 POST /v1/oauth/token called with anything other than grant_type=client_credentials.
account.invalid_external_identifier 409 The external_identifier supplied at registration is already used by another account. Safe to be specific here — it's the caller's own namespace, not an email-enumeration vector.
account.weak_password 400 Password fails the strength policy (minimum length and/or common-password blocklist — see core/common_passwords.py).
account.not_found 404 An account-scoped resource (API key, OAuth client) doesn't exist or doesn't belong to the authenticated account.
account.email_already_exists Reserved, never raised today. Exists in the taxonomy for a future context where confirming an email's existence carries no enumeration risk. POST /v1/accounts/register always returns a generic 202 regardless of whether the email is new, to avoid letting a caller enumerate registered accounts.
account.subaccount_credentials_incomplete 400 A subaccount's email/password were provided one without the other, on create or update (prompt 4). They must be given together or both left unset.

Freight Documents (prompt 3)

Code HTTP status Meaning
freight_document.invalid_transition 409 POST /v1/freightdocuments/{id}/issue (or any other call into FreightDocumentStateMachine) requested a transition not in the explicit allow-list — e.g. issuing a document that's already ISSUED.
freight_document.external_identifier_already_exists 409 POST /v1/freightdocuments supplied an external_identifier already used by another document — enforced by a DB unique constraint, not just an application check.
freight_document.not_found 404 Superseded for read/issue/role endpoints as of prompt 4 — those now return freight_document.permission_denied (403) uniformly, including for a document that doesn't exist at all, per the anti-enumeration principle below. Kept in the taxonomy as a defensive/internal-invariant code, not expected to be reachable through normal API use today.
freight_document.invalid_role_assignment 400 A role in the creation (or post-creation addition) payload has neither account_id nor email, has an empty/missing name in party_snapshot, or explicitly sets role_type: SUBMITTER (that role is derived automatically from the authenticated caller).

Roles & Permissions (prompt 4)

Code HTTP status Meaning
freight_document.permission_denied 403 The authenticated principal (account or subaccount) has no role on this freight document granting the permission the endpoint requires — including when the document doesn't exist at all. Deliberately never 404; see the anti-enumeration note below.
freight_document.role_already_exists 409 POST /v1/freightdocuments/{id}/roles tried to add a role type that's a singleton on this document (every type except SUBSEQUENTCARRIER) and one already exists.
freight_document.invalid_role_addition 400 POST /v1/freightdocuments/{id}/roles was called on a document not in ISSUED/TRANSIT status.

Freight Document CRUD (prompt 5)

Code HTTP status Meaning
freight_document.status_immutable_via_put 400 PUT /v1/freightdocuments/{id} body included a status different from the document's current one. Transitions go exclusively through the dedicated endpoints (issue, ...).
freight_document.immutable_field 400 PUT body included a value for hosting_type, external_host_platform, external_document_id, external_sync_status, submitter_account_id, or external_identifier that differs from the document's current one. Omitting these fields, or echoing back the current value, is fine.
freight_document.optimistic_lock_conflict 409 PUT/PATCH's version field didn't match the document's current version — either the application-level check caught a stale read, or the DB-level compare-and-swap (version_id_col) caught a race between the check and the write. Refetch and retry.
freight_document.version_required 400 PUT/PATCH was called without a version field. Deliberately a dedicated code, not a generic validation_errorversion is intentionally optional at the Pydantic level so this specific, common mistake gets a clear message.
freight_document.invalid_filter 400 GET /v1/freightdocuments's filters query param was malformed, named an unknown field, or used an operator not supported for that field.
freight_document.invalid_sort_field 400 GET /v1/freightdocuments's sort query param named a field outside the whitelist (created_at, updated_at, status).

Delegation & External Access (prompt 6)

Code HTTP status Meaning
freight_document.delegation_depth_exceeded 400 Creating a delegation would produce a depth greater than settings.max_delegation_depth (default 4).
freight_document.delegation_not_permitted 403 The caller doesn't hold DELEGATE on this exact role (directly or via an active delegation), or the role_id/freight_document_id pair in the URL don't match — same generic code either way, no distinguishing signal. Also returned for delegation/access-code revocation when the caller isn't the delegation's own delegator nor anyone upstream in its chain.
freight_document.delegation_chain_access_denied 403 GET /v1/freightdocuments/{id}/roles/{role_id}/delegations called by a principal not part of that specific role's delegation chain — including the document's own SUBMITTER/CONSIGNOR if they aren't a chain member.
freight_document.access_code_invalid 401 POST /v1/access-codes/redeem was called with a code that doesn't match any issued carrierAccessCode, or matches one that's been explicitly revoked.
freight_document.access_code_expired 401 The code matches an issued, non-revoked carrierAccessCode, but expires_at is in the past.

Directory (prompt 7)

Code HTTP status Meaning
partner.not_found 404 The partner_id in a URL path, or on a Freight Document role, doesn't exist, belongs to another account, or is soft-deleted. Deliberately identical for all three cases — never a distinguishing signal about another account's directory.
partner.external_identifier_already_exists 409 The account already has a Partner with this external_identifier — scoped to (owner_account_id, external_identifier), not global (contrast with account.invalid_external_identifier, which IS global).
partner.invalid_bulk_import 400 POST /v1/accounts/me/partners/bulk's array itself is malformed — currently only raised when it exceeds the per-request entry cap. A single entry's own failure (e.g. a duplicate external_identifier) is reported per-element in the response instead, not via this code.
packaging_method.not_found 404 The packaging method doesn't exist, or belongs to another account.
packaging_method.external_identifier_already_exists 409 Same scoping as partner.external_identifier_already_exists — not in the mission brief's literal error list, added by extension since the same DB constraint is mandated on this table too.
vehicle.not_found 404 The vehicle doesn't exist, belongs to another account, or is soft-deleted.
vehicle.external_identifier_already_exists 409 Same scoping and rationale as packaging_method.external_identifier_already_exists.
vehicle.invalid_subaccount_link 400 linked_subaccount_id doesn't reference a VEHICLE-type subaccount belonging to the same account — same generic code whether the type is wrong or the subaccount belongs to someone else.
driver.not_found 404 The driver doesn't exist, belongs to another account, or is soft-deleted.
driver.external_identifier_already_exists 409 Same scoping and rationale as packaging_method.external_identifier_already_exists.
driver.invalid_subaccount_link 400 linked_subaccount_id doesn't reference a DRIVER-type subaccount belonging to the same account.

Anti-enumeration note

Per the mission brief, no endpoint may reveal whether a given email is registered, nor whether a given freight document exists to a caller who isn't authorized to see it:

  • POST /v1/accounts/register always returns the same 202 response, whether the email is new or already registered (the existing-email case is a silent no-op — no second account, no second email).
  • POST /v1/accounts/login returns auth.invalid_credentials for both "no such account" and "wrong password" — same status code, same message. A dummy password hash is verified even when no account is found, so the response time doesn't leak which case it was.
  • POST /v1/accounts/password/reset-request always returns the same generic 200 message.
  • POST /v1/accounts/subaccounts/login (prompt 4) applies the same pattern: an unknown email, a subaccount with no password set (a traceability-only record), and a wrong password all return the identical auth.invalid_credentials.
  • GET /v1/freightdocuments/{id}, GET /v1/freightdocuments/ext/{external_identifier}, POST /v1/freightdocuments/{id}/issue, and POST /v1/freightdocuments/{id}/roles (prompt 4) all return freight_document.permission_denied (403), never 404, whether the document/identifier doesn't exist or exists but the caller has no role on it.
  • The delegation/access-code endpoints (prompt 6) apply the same principle to the {id}/{role_id} pair in their URLs: a role_id that doesn't exist, one that belongs to a different document than {id}, and one that exists on the right document but the caller has no DELEGATE access to, all return the identical freight_document.delegation_not_permitted (or delegation_chain_access_denied for the chain-read endpoint).
  • The directory (prompt 7) applies the same principle to every GET/PATCH/DELETE on a Partner/Vehicle/Driver/Packaging Method: an ID that doesn't exist and one that exists but belongs to a different account both return the identical *.not_found — never a signal that a resource exists in someone else's private directory. partner_id on a Freight Document role follows the same rule (partner.not_found either way).