Sellvik / developers
Storefront API

Customer auth

Email/password signup, login, refresh, logout for storefront customers.

Storefront customers authenticate with email + password per shop. Each shop has its own customer base; the same email can register at two shops independently. Google OAuth is in flight; OTP-via-SMS is currently panel-side only and not exposed through this API.

All four endpoints require a publishable key. Login/signup additionally trigger rate limits per source IP.


Signup

POST /api/v1/store/auth/signup

Request body

{
  "name": "Rafiul Hassan",
  "email": "rafiul@example.com",
  "password": "correct horse battery staple",
  "phoneNumber": "+8801711000000"
}
FieldTypeRequiredNotes
namestringyes1–100 chars.
emailstringyesLowercased and trimmed server-side.
passwordstringyesMin 8 chars. Stored as Argon2id hash.
phoneNumberstringnoE.164 format.

Response

HTTP/1.1 201 Created

{
  "customer": {
    "id": "u1b2c3d4-...",
    "name": "Rafiul Hassan",
    "email": "rafiul@example.com",
    "phoneNumber": "+8801711000000",
    "imageUrl": null,
    "createdAt": "2026-05-27T14:00:00.000Z"
  },
  "tokens": {
    "accessToken": "eyJhbGciOiJIUzI1Ni...",
    "accessTokenExpiresAt": "2026-05-27T15:00:00.000Z",
    "refreshToken": "eyJhbGciOiJIUzI1Ni...",
    "refreshTokenExpiresAt": "2026-06-26T14:00:00.000Z"
  }
}

Errors

StatusCodeWhen
400invalid_bodyValidation failed.
409email_existsA customer with that email already exists in this shop.
429rate_limited5 signups per minute per IP.

Login

POST /api/v1/store/auth/login

Request body

{
  "email": "rafiul@example.com",
  "password": "correct horse battery staple"
}

Response

Same shape as signup.

Errors

StatusCodeWhen
401invalid_credentialsWrong email or password. Generic to prevent enumeration.
429rate_limited10 attempts per minute per IP.
423account_lockedToo many failed attempts. Locked for 15 minutes.

Refresh

POST /api/v1/store/auth/refresh

Exchange the refresh token for a new access + refresh pair. Refresh tokens are single-use.

Request body

{ "refreshToken": "eyJhbGciOiJIUzI1Ni..." }

Response

{
  "tokens": {
    "accessToken": "...",
    "accessTokenExpiresAt": "...",
    "refreshToken": "...",
    "refreshTokenExpiresAt": "..."
  }
}

Errors

401 invalid_customer_token with reason:

  • expired — refresh token TTL elapsed (30 days). Customer must re-login.
  • revoked — the token chain was revoked.
  • replayed — the refresh token was already exchanged. The entire token family is now revoked. Treat as credential compromise. The customer must re-login.
  • invalid — token doesn't parse or signature mismatch.

Logout

POST /api/v1/store/auth/logout

Revoke the customer's refresh token chain server-side. The access token will continue to work until natural expiry (up to 1 hour) — drop it client-side.

Request body

{ "refreshToken": "eyJhbGciOiJIUzI1Ni..." }

Response

204 No Content.


What happens when an access token expires

API returns 401 invalid_customer_token with reason: "expired". Call refresh, retry the original request with the new access token.

async function callAuthed(req: () => Promise<Response>) {
  let res = await req()
  if (res.status === 401) {
    const { error } = await res.json()
    if (error.code === "invalid_customer_token" && error.reason === "expired") {
      await refresh()
      res = await req()
    }
  }
  return res
}

Don't refresh proactively on a timer — clock skew across devices makes this brittle. Let the 401 trigger the refresh.

On this page