Skip to content

Authentication

The V3 API uses OAuth2 bearer tokens. Every authenticated request sends:

Authorization: Bearer <accessToken>

Tokens are minted by the SMS-PIN flow: the user proves they own a phone number, and the server issues a token tied to their account. No grant-type, client-id, or client-secret handling on the client side — the auth server takes care of all of that.

Submit the user’s email + a reCAPTCHA v3 token. The server:

  • Looks up the user.
  • If found: sends an SMS PIN to the phone on file. Responds with { exists: true }.
  • If not found: responds with { exists: false } so your UI can switch to the registration path.
{
"email": "alice@example.com",
"captchaToken": "03AGdBq25..."
}

For users who don’t exist yet, submit profile fields and a phone number. The server creates the account and sends the SMS PIN.

{
"firstname": "Alice",
"lastname": "Smith",
"email": "alice@example.com",
"mobile": "3035551212",
"countryCode": "US",
"captchaToken": "03AGdBq25..."
}

Submit the email + the PIN the user read from SMS. On success the response contains the bearer token.

{
"accessToken": "ad5e3f...",
"refreshToken": "...",
"expiresIn": 2592000
}

expiresIn is in seconds. Tokens live for 30 days.

/v3/auth/email-check and /v3/auth/register expect a reCAPTCHA v3 token. We’ve been the target of bot-driven SMS-cost attacks; the captcha is the front line.

You don’t need to pay for a separate site key — request one from engineering@handbid.com and we’ll issue a Handbid-scoped key.

Requests that arrive without a captcha token (or with an invalid one) fall back to a stricter rate limit — 10 requests per 5 minutes per IP per endpoint. This is the defense against bots that script the SMS-PIN flow at scale.

If you’re hitting that throttle in legitimate traffic, you’re probably missing the captcha token. Double-check the request body.

The bearer token is sensitive. Treat it like a password.

  • Mobile clients: iOS Keychain, Android EncryptedSharedPreferences, whatever your platform’s secure-storage primitive is.
  • Web clients: httpOnly cookie scoped to your domain. Not localStorage.
  • Server-to-server integrations: a secrets manager (1Password, Vault, AWS Secrets Manager). Not a .env file checked into git.

If a token leaks, the only recourse is to mint a new one via the SMS flow. Revocation is on the roadmap; today, the practical mitigation is “make expiration short and rotate often.” 30 days is the current default but you can request a shorter window per-integration if your security posture requires it.

The refreshToken returned alongside the access token can be exchanged for a new access token without re-prompting the user for an SMS PIN. The endpoint is /auth/refresh-token on the V1 surface — V3 hasn’t reimagined this yet. If you need the V1 refresh contract, ping us.

A small subset of V3 endpoints accept optional authentication — mostly public discovery (/v3/auctions, /v3/auctions/{id}, /v3/item/{id}). Anonymous callers get the same response shape minus user-scoped fields like isFavorite.

Whether a request was anonymous or authenticated affects rate limits — see Rate limiting for the keying logic.

A missing, expired, or malformed token returns:

{
"error": "unauthorized",
"message": "Your request was made with invalid credentials."
}

If you see a 401 on what you expected to be a fresh token, the most common causes are:

  1. Token expired — check expiresIn and refresh proactively.
  2. Token minted for the wrong OAuth client ID. V3 requires ios, android, web, ogac, vt, connectNGo, or zap.
  3. Bearer prefix missing or malformed (Authorization: <token> instead of Authorization: Bearer <token>).
  • POST /v3/auth/email-check
  • POST /v3/auth/register
  • POST /v3/auth/verify-pin

Each route’s exact request and response shape is documented in the API Reference.