Skip to content

Errors

Every V3 endpoint returns the same error envelope on any non-2xx response:

{
"error": "<code>",
"message": "<human-readable description>"
}

429 responses add a retryAfter integer (seconds). All other fields are optional and stable — adding a new top-level key in a future version won’t break existing clients.

Statuserror codeMeaningWhat to do
400validation_errorYour request was malformed — bad params, unknown filter token, out-of-range value.Read message for the specific issue. Fix the request before retrying.
401unauthorizedToken missing, expired, or minted for the wrong client.Refresh the token via the SMS flow or refresh_token exchange.
403captcha_failedCaptcha token missing or invalid on a gated endpoint.Acquire a new reCAPTCHA token before retrying.
404not_foundThe resource doesn’t exist or isn’t visible to the caller.Don’t retry. Check the ID.
409email_existsRegistration attempted with an email that’s already registered.Switch to the login path (/v3/auth/email-check).
429rate_limitedPer-endpoint rate limit exceeded.Honor retryAfter (seconds). Don’t retry sooner.
500server_errorUnexpected failure on our side.Treat as transient. Exponential backoff is appropriate.

A typical 400 from the notifications endpoint:

HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "validation_error",
"message": "`filter` contains unknown type \"closing_soon\". Allowed: outbid, winning, purchase, broadcast, general."
}

A typical 429 from the same endpoint after a burst:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
Access-Control-Expose-Headers: Retry-After
{
"error": "rate_limited",
"message": "Rate limit exceeded. Retry after 60 seconds.",
"retryAfter": 60
}

Both the header and the body field carry retryAfter. Use whichever is easier on your client — they always match.

The message field is for humans. It’s safe to render in a UI as a toast or alert. It tells the user what went wrong in language they can act on:

  • ✅ “Auction not found.”
  • ✅ “limit must be between 0 and 100.”
  • ❌ Not a stack trace.
  • ❌ Not a SQL error.
  • ❌ Not a token or any other secret.

The error field is for your code. Branch on it for retry logic, custom UI, or telemetry tags. Don’t pattern-match on message — that’s unstable across versions.

Write endpoints in V3 are idempotent by intent rather than by idempotency-key header. Specifically:

  • POST /v3/item/{id}/bid — placing the same bid twice in quick succession may legitimately fail the second time (“already winning”). That’s not an idempotency failure — it’s the server protecting you from double-bidding.
  • POST /v3/bidder/favorite/{id} — calling on an already-favorited item succeeds with no side effects.
  • DELETE /v3/bidder/favorite/{id} — calling on a non-favorited item succeeds with no side effects.
  • PUT /v3/bidder/notifications/read-all — calling when there’s nothing unread returns { "count": 0 }. Always safe to call.

Treat these as the contract. If your client retries a failed-network bid and the bid actually went through, the second call will tell you the right state — not a duplicate-bid error.

  • 2xx: done.
  • 400 / 403 / 404 / 409: do not retry. The error is in your request, not ours. Fix and resubmit.
  • 401: refresh the token, then retry once. If 401 again, drop the session.
  • 429: honor retryAfter. Don’t dispatch another request to the same endpoint until that many seconds have passed.
  • 5xx: retry with exponential backoff (e.g., 1s, 2s, 4s, 8s, capped at 60s) up to 4 attempts. After that, surface the error to the user.

If you see a 500 with a message that looks like a true bug:

  1. Capture the response headers (including any x-request-id).
  2. Note the timestamp in UTC.
  3. Email engineering@handbid.com with both. We can usually trace the request to the offending log line in under a minute.