Errors

Every error response from the API follows RFC 9457 Problem Details for HTTP APIs. The code field is the stable contract — the mobile client switches on it to drive UX.

Body shape

{
  "type": "https://api.swappr.co.uk/errors/validation-failed",
  "title": "Validation failed",
  "status": 400,
  "code": "VALIDATION_FAILED",   // enum: see catalog below
  "detail": "bedrooms must be between 0 and 10",
  "instance": "/api/v1/listings",
  "requestId": "01HZQ7K3M4N5P6Q7R8S9T0V1W2",
  "errors": [                    // present only for VALIDATION_FAILED
    {
      "path": "bedrooms",
      "message": "Out of range",
      "code": "OUT_OF_RANGE"
    }
  ],
  "meta": {                       // optional; only on errors that carry structured context
    "deletionScheduledAt": "2026-07-14T02:45:54.729Z"
  }
}
Content-Type is application/problem+json. The optional meta object (an RFC 9457 extension member) appears only on errors that carry machine-readable context the client must act on — currently ACCOUNT_PENDING_DELETION, which sets meta.deletedAt and meta.deletionScheduledAt.

Error catalog

Every code in the codebase appears in this table. When an endpoint can return non-obvious codes, its reference page lists them in an Error responses section.
CodeHTTPMeaningClient should
VALIDATION_FAILED400Request payload didn’t match schemaShow field errors to user
PAYLOAD_TOO_LARGE413Body or file too bigReduce size and retry
UNAUTHENTICATED401Token missing/invalid/expiredRefresh, then retry once; on second 401, log out
TOKEN_REVOKED401Refresh token reused or logout-all invokedLog out, redirect to login
INVALID_CODE4016-digit OTP digits are wrong (attempts++)Re-prompt; show attempts remaining
CODE_EXPIRED401OTP expired, consumed, attempt-capped, or email unknownRequest a fresh code via /resend-verify-email
OAUTH_INVALID_TOKEN401Social-login ID token failed verification (malformed, expired, wrong project) or carried no emailRe-run native sign-in to mint a fresh token
PAYWALL_REQUIRED402Subscription required after launch phaseShow paywall sheet
FORBIDDEN403Authenticated but not allowedShow “no permission” UI
ONBOARDING_INCOMPLETE403User must finish onboardingRedirect to onboarding step
TENANCY_PENDING403Tenancy doc awaiting admin approvalShow waiting state
TENANCY_REJECTED403Tenancy was rejectedShow re-upload UI
ACCOUNT_BANNED403Account bannedForce logout, show ban screen
ACCOUNT_PENDING_DELETION403Credentials are correct but the account is soft-deleted and inside the 30-day erasure grace window. Carries meta.deletedAt + meta.deletionScheduledAt. Returned by /auth/login.Route to the reactivation screen; offer POST /auth/reactivate
NOT_FOUND404Resource doesn’t existShow empty/missing state
STATE_CONFLICT409Resource in wrong state for operationRefresh data, retry
ALREADY_HAS_LISTING409One-listing-per-user uniqueness violatedUse PATCH on existing
EMAIL_ALREADY_REGISTERED409Email exists with a different passwordDirect user to /login or /forgot-password
ACCOUNT_EXISTS_VIA_OAUTH409Email is bound to a social-login identityShow generic “use your social login” CTA (response does NOT disclose which provider)
IDEMPOTENCY_KEY_REUSED409Idempotency-Key already used with different payloadUse a fresh key
RATE_LIMITED429Too many requestsRespect Retry-After header
INTERNAL_ERROR500Unexpected server failureRetry once; report requestId
UPSTREAM_FAILURE502Dependency failed (Mongo, Redis, etc.)Retry with backoff
SERVICE_UNAVAILABLE503Service temporarily downRetry with backoff
STORAGE_NOT_CONFIGURED503DO Spaces creds not provisioned (dev only)Surface to ops
OAUTH_NOT_CONFIGURED503Firebase creds (FCM_SERVICE_ACCOUNT_JSON + FIREBASE_PROJECT_ID) not provisioned (dev only)Surface to ops
MAIL_NOT_CONFIGURED503Resend creds not provisioned (dev only)Surface to ops
PUSH_NOT_CONFIGURED503FCM/APNs not provisioned (dev only)Surface to ops

How errors map server-side

  • A controller or service throws AppError(code) from @swappr/shared.
  • The Express error middleware converts it to the Problem Details JSON above.
  • Anything that ISN’T an AppError (library exceptions, runtime crashes) maps to INTERNAL_ERROR with detail: "An unexpected error occurred. Please try again later." — the underlying cause is logged server-side but never serialized to the client.