{"openapi":"3.0.3","info":{"title":"HUB API","version":"1.0.0","description":"REST API for HUB - a multi-vendor e-commerce marketplace for the Kenyan market. Handles authentication, user management, merchant onboarding, product listings, orders, and MPESA payments.","contact":{"name":"HUB","email":"support@thehub.co.ke"}},"servers":[{"url":"http://localhost:3000","description":"Local development"},{"url":"https://api.thehub.co.ke","description":"Production"}],"tags":[{"name":"Authentication","description":"Register, login, token management, email verification, password reset, and 2FA"},{"name":"Users","description":"User profile management"},{"name":"Merchants","description":"Merchant applications and shop management"},{"name":"Products","description":"Product listings"},{"name":"Orders","description":"Order management and tracking"},{"name":"Payments","description":"MPESA payment integration"},{"name":"Admin","description":"Platform administration"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Access token issued on login or email verification. Valid for 15 minutes."}},"schemas":{"SuccessResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object"}}},"ErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string"},"details":{"type":"array","items":{"type":"object"},"nullable":true}}},"UserProfile":{"type":"object","properties":{"id":{"type":"string","example":"clxyz123"},"name":{"type":"string","example":"Jane Doe"},"email":{"type":"string","example":"jane@example.com"},"role":{"type":"string","enum":["USER","MERCHANT","ADMIN"]}}},"TokenPair":{"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token (15m TTL)"},"refreshToken":{"type":"string","description":"Opaque refresh token (7d TTL)"},"user":{"$ref":"#/components/schemas/UserProfile"}}},"RegisterRequest":{"type":"object","required":["name","email","password"],"properties":{"name":{"type":"string","minLength":2,"maxLength":80,"example":"Jane Doe"},"email":{"type":"string","format":"email","example":"jane@example.com"},"password":{"type":"string","minLength":8,"maxLength":128,"example":"Sup3rS3cure!"},"phone":{"type":"string","example":"+254712345678","nullable":true}}},"LoginRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email","example":"jane@example.com"},"password":{"type":"string","example":"Sup3rS3cure!"}}},"EmailCodeRequest":{"type":"object","required":["email","code"],"properties":{"email":{"type":"string","format":"email"},"code":{"type":"string","minLength":6,"maxLength":6,"example":"482910"}}},"EmailRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}},"ResetPasswordRequest":{"type":"object","required":["email","code","newPassword"],"properties":{"email":{"type":"string","format":"email"},"code":{"type":"string","minLength":6,"maxLength":6},"newPassword":{"type":"string","minLength":8,"maxLength":128}}},"ChangePasswordRequest":{"type":"object","required":["currentPassword","newPassword"],"properties":{"currentPassword":{"type":"string"},"newPassword":{"type":"string","minLength":8,"maxLength":128}}},"RefreshRequest":{"type":"object","required":["refreshToken"],"properties":{"refreshToken":{"type":"string"}}},"LogoutRequest":{"type":"object","required":["refreshToken"],"properties":{"refreshToken":{"type":"string"}}},"TwoFactorVerifyRequest":{"type":"object","required":["code"],"properties":{"code":{"type":"string","minLength":6,"maxLength":6,"example":"291047"}}}},"responses":{"Unauthorized":{"description":"Missing or invalid access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"ValidationError":{"description":"Request body failed validation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"TooManyRequests":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"paths":{"/api/auth/register":{"post":{"tags":["Authentication"],"summary":"Register","description":"Create a new user account. A 6-digit verification code is sent to the provided email. The account cannot be used until the email is verified via `/api/auth/verify-email`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}}},"responses":{"201":{"description":"Account created - verification code sent","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessResponse"},{"properties":{"data":{"properties":{"message":{"type":"string"}}}}}]}}}},"409":{"description":"Email already in use","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/auth/verify-email":{"post":{"tags":["Authentication"],"summary":"Verify email","description":"Submit the 6-digit OTP code sent to the user's email after registration. On success, returns an access + refresh token pair and activates the account.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailCodeRequest"}}}},"responses":{"200":{"description":"Email verified - tokens issued","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessResponse"},{"properties":{"data":{"$ref":"#/components/schemas/TokenPair"}}}]}}}},"400":{"description":"Invalid or expired code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"Login","description":"Authenticate with email and password. Returns an access token (15m) and a refresh token (7d). The email must be verified before login.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Login successful","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessResponse"},{"properties":{"data":{"$ref":"#/components/schemas/TokenPair"}}}]}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Email not verified or account suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"Logout","description":"Revoke the provided refresh token. The access token will expire naturally (15m TTL).","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutRequest"}}}},"responses":{"200":{"description":"Logged out successfully"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh tokens","description":"Exchange a valid refresh token for a new access + refresh token pair. The old refresh token is revoked immediately (rotation). If you replay a revoked token, it will be rejected.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshRequest"}}}},"responses":{"200":{"description":"New token pair issued","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessResponse"},{"properties":{"data":{"properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"}}}}}]}}}},"401":{"description":"Token invalid, expired, or revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/auth/resend-verification":{"post":{"tags":["Authentication"],"summary":"Resend verification code","description":"Resend the email verification code. Previous codes are invalidated. Always returns a success message to prevent email enumeration.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRequest"}}}},"responses":{"200":{"description":"Code sent (or silently ignored if email is already verified)"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/auth/forgot-password":{"post":{"tags":["Authentication"],"summary":"Forgot password","description":"Send a 6-digit password reset code to the user's email. Always returns the same message to prevent email enumeration.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailRequest"}}}},"responses":{"200":{"description":"Reset code sent (or silently ignored if email not found)"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/auth/reset-password":{"post":{"tags":["Authentication"],"summary":"Reset password","description":"Reset the user's password using the 6-digit OTP code. On success, all active refresh tokens are revoked (forces re-login on all devices).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResetPasswordRequest"}}}},"responses":{"200":{"description":"Password reset successfully"},"400":{"description":"Invalid or expired code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/users/me/password":{"post":{"tags":["Authentication"],"summary":"Change password","description":"Change the authenticated user's password. Requires the current password for confirmation. Rate limited to 5 attempts per 15 minutes, per user and per IP, because verifying the current password makes this a guessing surface.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}}},"responses":{"200":{"description":"Password changed successfully"},"400":{"description":"Current password incorrect, or new password same as current","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/users/me":{"delete":{"tags":["Authentication"],"summary":"Delete own account","description":"Permanently delete the authenticated user's account (App Store guideline 5.1.1(v)). Requires the current password. Refused with 409 while money or goods are still moving on the account (an order in PAID..OUT_FOR_DELIVERY, an active courier delivery, an open dispute, a shop or courier wallet balance, or a payout in flight); `blockers` lists human-readable reasons. On success unpaid CART/PENDING_PAYMENT orders are cancelled, every session and push token is revoked, and the profile is anonymised. Orders, payments, ledger and reviews are kept. Rate limited to 5 attempts per 15 minutes.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["password"],"properties":{"password":{"type":"string"},"reason":{"type":"string","maxLength":1000}}}}}},"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"deleted":{"type":"boolean","example":true}}}}}}}},"401":{"description":"Missing/invalid token, or `Incorrect password`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Deletion blocked","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string"},"blockers":{"type":"array","items":{"type":"string"}}}}}}},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/auth/2fa/send":{"post":{"tags":["Authentication"],"summary":"Send 2FA code","description":"Send a 6-digit two-factor authentication code to the authenticated user's email. Previous unused codes are invalidated.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"2FA code sent"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/auth/2fa/verify":{"post":{"tags":["Authentication"],"summary":"Verify 2FA code","description":"Verify the 6-digit two-factor authentication code. The code has a 10-minute TTL and a maximum of 5 attempts.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TwoFactorVerifyRequest"}}}},"responses":{"200":{"description":"2FA verified","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessResponse"},{"properties":{"data":{"properties":{"message":{"type":"string"},"verified":{"type":"boolean"}}}}}]}}}},"400":{"description":"Invalid or expired code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}}}}