API Reference¶
One HTTP API: the Go/Fiber backend, all routes under the base path
/api/v1 (aim-backend/internal/handler/routes.go).
| API | Base URL | Auth | Source |
|---|---|---|---|
| Backend API | https://api.aimstoreorg.com/api/v1 |
custom JWT or Firebase ID token; some routes public | aim-backend/ |
The website's Express payment server (aim-store-website/server/index.js)
additionally exposes gateway endpoints under https://aimstoreorg.com/api/*
(CCAvenue/PhonePe initiate/response/webhook) — those are gateway plumbing,
not part of the /api/v1 surface.
Conventions¶
- Auth: protected routes go through a dual-auth middleware
(
internal/middleware/firebase_auth.go) that accepts eitherx-sdk-access-token: <custom JWT>(issued by the OTP and admin-login flows) orAuthorization: Bearer <firebase_id_token>. Rows below say public, token, or admin. - Admin: admin routes additionally require the JWT claim
role == "admin"(set fromusers.is_admin); anything else is403. - Errors: non-2xx responses carry
{"error": "message"}. Some rate-limited endpoints add acodefield (e.g.OTP_RATE_LIMITED). - Responses are plain JSON maps built per handler — there is no global
envelope. Read the handler file in
aim-backend/internal/handler/for the exact shape of a given endpoint. /metrics(Prometheus) and/healthexist at the app root;/metricsis blocked publicly at Caddy.
Health & meta¶
| Method | Path | Auth | Summary |
|---|---|---|---|
| GET | /health |
public | Liveness probe → {status:"ok"}. (App root, not under /api/v1.) |
| GET | /app/version |
public | App-version gate for the mobile client → {version, apk_url, force_update, …}. |
Auth¶
| Method | Path | Auth | Summary |
|---|---|---|---|
| POST | /auth/register |
public | Create account with {phone, password, full_name}; creates the wallet → 201 {custom_token}. 409 duplicate phone. Optional referral_code attributes the signup to a referrer (invalid codes are silently ignored — never a registration failure). |
| POST | /auth/login |
public | Phone + password login → {custom_token} (Firebase custom token). |
| POST | /auth/admin/login |
public | New. Password login for the admin portal: {phone, password} → {access_token, user} where user.is_admin is true. Uniform 401 on any failure; 429 after 10 attempts per phone+IP per 15 min. |
| POST | /auth/reset-password |
public | Reset password (OTP-verified). |
| POST | /auth/otp/send |
public | Send a login OTP via SMS; resend cooldown + per-phone send cap (429 OTP_RATE_LIMITED). |
| POST | /auth/otp/verify |
public | Verify OTP; auto-creates user + wallet on first signup (gated by purpose) → tokens + user (incl. email_verified). Optional referral_code + referral_source (install_referrer | manual) attribute a new user's signup; absent those, an IP match against recent /r/:code clicks is tried. Invalid codes never fail signup. |
| POST | /auth/refresh |
public | Refresh the custom JWT. |
| POST | /auth/logout |
token | Revoke the caller's active tokens server-side. |
| GET | /me |
token | Caller profile (incl. email_verified, true iff the current email was OTP-verified). |
| PATCH | /me |
token | Update own profile fields. Changing email to a different value clears its verified status. |
Email verification¶
Authenticated OTP flow that proves ownership of the caller's email address.
Gated on SMTP configuration (SMTP_*) — unconfigured, send-otp returns
503 {"error":"email_disabled"}.
| Method | Path | Auth | Summary |
|---|---|---|---|
| POST | /me/email/send-otp |
token | Send a 6-digit OTP to {email} (10-min TTL). 409 if the email belongs to a different user; 60s resend cooldown, max 5 sends/hour per user and 20/hour per IP. |
| POST | /me/email/verify-otp |
token | Verify {otp} (max 5 attempts, then the OTP is burned); on success sets the email + verified timestamp → {email, email_verified: true}. |
Referrals¶
Every user can lazily mint a referral code and share
https://aimstoreorg.com/r/<CODE>. Attribution happens at signup (see
/auth/otp/verify and /auth/register above) — explicit code first, IP
match against recent clicks as fallback.
| Method | Path | Auth | Summary |
|---|---|---|---|
| GET | /r/:code |
public | Referral share-link redirect: records the click (code, client IP, user-agent) and 302s to the Play Store with the code encoded in the install-referrer query. Unknown/malformed codes still redirect (no click recorded). 20/hour per IP. (App root on aimstoreorg.com — routed by Caddy to the API — not under /api/v1.) |
| GET | /referrals/my-code |
token | Caller's referral code, lazily generated on first call → {referral_code, share_url}. |
| GET | /referrals/check |
public | Most recent referral click from the caller's IP within the attribution window (default 48h) → {referral_code} (or null). 30/hour per IP. |
Account deletion¶
Google Play requires a deletion route that works without installing or
signing into the app, so the web/ flow is unauthenticated — each step is
gated on an OTP sent to the registered mobile.
| Method | Path | Auth | Summary |
|---|---|---|---|
| POST | /account/deletion/web/send-otp |
public | Start web deletion: send OTP to the registered phone. |
| POST | /account/deletion/web/verify |
public | Verify the web-deletion OTP. |
| POST | /account/deletion/web/request |
public | File the deletion request (OTP-proven). |
| GET | /account/deletion/eligibility |
token | Can this account be deleted (open orders/balances)? |
| POST | /account/deletion/send-otp |
token | Send in-app deletion OTP. |
| POST | /account/deletion/verify-otp |
token | Verify the deletion OTP. |
| POST | /account/deletion/request |
token | Request deletion for the caller's own account. |
| GET | /account/deletion/status |
token | Deletion request status. |
| POST | /account/deletion/cancel |
token | Cancel a pending deletion. |
Catalogue (public storefront)¶
| Method | Path | Auth | Summary |
|---|---|---|---|
| GET | /products |
public | Product catalogue. |
| GET | /products/:id |
public | One product. |
| GET | /categories |
public | Category list. |
Catalogue images uploaded through the admin portal are served (outside
/api/v1) at GET /media/:key — public and unauthenticated, because these
are the product photos every storefront visitor already sees. The key is the
content hash minted at upload, so the bytes under it never change and the
response is Cache-Control: public, max-age=31536000, immutable.
Each product carries both a single cover image and its full gallery (ADR 0013):
{
"id": "…", "name": "Circle (Ai-ENC) TWS Earbuds",
"image_url": "https://cdn…/01.jpg",
"images": [
{ "id": "…", "image_url": "https://cdn…/01.jpg", "alt_text": "tws earphones", "sort_order": 0 },
{ "id": "…", "image_url": "https://cdn…/02.jpg", "alt_text": "tws earphones", "sort_order": 1 }
]
}
images is ordered by sort_order, always present ([] when the product has
no images, never null), and images[0].image_url always equals image_url.
A client that only wants one image can keep reading image_url alone.
Wallet¶
| Method | Path | Auth | Summary |
|---|---|---|---|
| GET | /wallet |
token | Balance (shopping credits, reward points, e-vouchers). |
| GET | /wallet/transactions |
token | Transaction history. |
| POST | /wallet/topup-initiate |
token | Start a top-up: mints a one-time token → hosted-page URL. 503 until TOPUP_PAGE_BASE_URL is configured. |
| GET | /wallet/topup-status/:token |
token | Poll a top-up; verifies directly with PhonePe and credits if the callback hasn't landed yet. |
| POST | /wallet/topup-credit |
public (HMAC) | Gateway-confirmed credit callback from the website payment server; mandatory HMAC over STORE_WEBHOOK_SECRET (503 if unconfigured). |
| POST | /wallet/settle-pending |
token | Compatibility no-op (pending buckets retired with the gaming currencies) → {wallet, settled: 0}. |
| POST | /wallet/daily-spin |
token | Free daily Spin & Win → credits shopping credits; prize amount is a server-side constant. |
No direct top-up route
POST /wallet/topup is intentionally not routed — it credited
shopping credits with no payment. Money enters only through
topup-initiate → hosted page → verified topup-credit / topup-status.
Do not re-add it (see the comment in routes.go).
Orders & payment gateways¶
| Method | Path | Auth | Summary |
|---|---|---|---|
| GET | /payment-gateways |
token | Active gateways the client may offer. |
| POST | /orders/quote |
token | Server-authoritative price quote for a cart. |
| POST | /orders |
token | Place an order paid from the shopping-credits wallet. |
| GET | /orders |
token | List the caller's orders. |
| GET | /orders/:id |
token | One order. |
| POST | /orders/:id/cancel |
token | Cancel an order (refunds the wallet). |
Support tickets¶
| Method | Path | Auth | Summary |
|---|---|---|---|
| POST | /tickets |
token | Open a ticket (email notification via SMTP if configured). |
| GET | /tickets |
token | List own tickets. |
| GET | /tickets/:id |
token | One ticket with replies. |
| POST | /tickets/:id/reply |
token | Reply to own ticket. |
Admin (/api/v1/admin/*)¶
All routes below require a token whose JWT claim role == "admin"
(internal/middleware/firebase_auth.go, AdminOnly). The admin portal
obtains such a token from POST /auth/admin/login.
Catalogue & payment gateways¶
| Method | Path | Summary |
|---|---|---|
| GET | /admin/products |
Full catalogue (incl. inactive). |
| POST | /admin/products |
Create a product. |
| PUT/DELETE | /admin/products/:id |
Edit/delete a product. |
| GET/PUT | /admin/products/:id/cost |
Read/set the admin-only unit cost. |
| GET | /admin/categories |
Category list. |
| GET/POST | /admin/payment-gateways |
List/create gateways. |
| PUT/DELETE | /admin/payment-gateways/:id |
Edit/delete a gateway. |
Catalogue image upload¶
POST /api/v1/admin/uploads/image stores an image and returns the URL to save
on a product or category:
{ "filename": "circle-black.jpg", "data": "data:image/jpeg;base64,/9j/4AAQ…" }
data may be bare base64 or a full data: URL (what FileReader.readAsDataURL
produces). Base64 rather than multipart because the admin browser reaches the
API through the portal's Next.js proxy, which reads the body as text.
{ "data": { "url": "https://api.aimstoreorg.com/media/<sha256>.jpg",
"content_type": "image/jpeg", "bytes": 148213 } }
The stored name is the content hash, so re-uploading identical bytes
returns the same URL instead of duplicating the file. The extension and
content_type come from sniffing the bytes — filename and any type
declared in a data: URL are ignored. PNG, JPEG, GIF and WebP are accepted;
SVG is refused (it is XML that can carry script, and it would execute on the
API origin). Limits: 413 IMAGE_TOO_LARGE over 5 MB, 400
UNSUPPORTED_IMAGE_TYPE otherwise. A server with no media directory configured
answers 503 uploads_disabled, and the admin falls back to pasting a URL.
Product create/update accept an images array of
{image_url, alt_text} in display order. The server trims and de-duplicates
it, renumbers sort_order densely from 0, caps it at 12 rows, and forces
image_url to the first entry — whatever the request sent for image_url is
ignored when images is supplied. Omitting images leaves an existing
gallery untouched (the pre-gallery client path); sending [] clears it.
Users, dashboard, orders¶
| Method | Path | Summary |
|---|---|---|
| GET | /admin/dashboard |
Headline platform metrics. |
| GET | /admin/transactions |
All wallet transactions. |
| GET | /admin/users |
User list/search. |
| GET | /admin/users/:uid |
One user. |
| PUT | /admin/users/:uid |
Edit a user (incl. is_admin). |
| DELETE | /admin/users/:uid |
Delete a user. |
| GET | /admin/users/:uid/ledger · /orders · /tickets |
Per-user drill-downs. |
| GET | /admin/webhook/logs |
Payment-webhook log. |
| GET | /admin/orders |
All store orders. |
| POST | /admin/orders/:id/status |
Update an order's status. |
| POST | /admin/orders/:id/refund |
Refund an order to the wallet. |
| GET | /admin/refunds |
Refund list. |
| GET | /admin/reconciliation |
Payments/orders reconciliation view. |
Tickets¶
| Method | Path | Summary |
|---|---|---|
| GET | /admin/tickets |
All tickets. |
| GET | /admin/tickets/:id |
One ticket. |
| PUT | /admin/tickets/:id/status |
Update ticket status. |
| POST | /admin/tickets/:id/reply |
Reply as support. |