Customer Auth Flow
Customer authentication uses a 3-step email OTP flow. There are no passwords.
Step 1: Request OTP
POST /access/api/v1/auth
Body:
{ "email": "customer@example.com" }
The API sends a 6-digit code to the customer's email and returns a verification id:
{
"status": "success",
"data": { "id": "69e885ba4f0e4f248bc572fe" }
}
Store the id in component state — you need it in step 2.
Possible errors:
| Code | What to show |
|---|---|
ACCOUNT_BLACKLISTED | "Your account has been suspended" |
TOO_MANY_LOGIN_ATTEMPTS | "Too many attempts, please wait before trying again" |
AUTH_NOT_INITIALIZED | "Login is not available for this store" |
Step 2: Verify OTP
POST /access/api/v1/auth/verify
Body:
{
"email": "customer@example.com",
"id": "69e885ba4f0e4f248bc572fe",
"code": "217273",
"event": "login"
}
On success, returns the JWT token:
{
"status": "success",
"data": { "token": "eyJhbGci..." }
}
Possible errors:
| Code | What to show |
|---|---|
INVALID_CODE | "Incorrect code, please try again" |
VERIFICATION_NOT_VALID | "Code has expired, please request a new one" |
TOO_MANY_VERIFY_ATTEMPTS | "Too many attempts, please request a new code" |
Step 3: Store and use the token
localStorage.setItem('dn_token', data.token);
Pass the token on all protected requests:
Authorization: Bearer eyJhbGci...
Getting the logged-in customer
GET /access/api/v1/auth
Authorization: Bearer <token>
Returns the customer's full profile.
Updating the profile
PATCH /access/api/v1/auth
Authorization: Bearer <token>
Body:
{
"name": "Jane Doe",
"phone": "0712345678",
"town": "Nairobi",
"city": "",
"apartment": "Westlands",
"room": "4B",
"postalCode": ""
}
name, phone, and town are required. The rest are optional address fields.
Magic link login
Customers can log in from a link in their order confirmation email without entering a code. This is the "view your order" flow.
POST /access/api/v1/auth/access
Body:
{
"id": "link_id",
"email": "customer@example.com",
"order": "order_id"
}
Returns the same JWT token as the OTP flow.
Possible errors:
| Code | What to show |
|---|---|
INVALID_ACCESS_LINK | "This link is invalid" |
ACCESS_LINK_EXPIRED | "This link has expired" |
ORDER_CANCELLED | "This order was cancelled" |
Logout
A server-side logout endpoint is not yet available. For now, logout is handled client-side — delete the token from storage and clear any customer state:
localStorage.removeItem('dn_token');
router.push('/');
Handling expired tokens
When any protected request returns SESSION_EXPIRED or AUTH_TOKEN_INVALID, clear the token and redirect to login. If you want to preserve the customer's destination, pass it as a query param:
router.push(`/login?redirect=${encodeURIComponent(currentPath)}`);