Skip to content

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 either x-sdk-access-token: <custom JWT> (issued by the OTP and admin-login flows) or Authorization: Bearer <firebase_id_token>. Rows below say public, token, or admin.
  • Admin: admin routes additionally require the JWT claim role == "admin" (set from users.is_admin); anything else is 403.
  • Errors: non-2xx responses carry {"error": "message"}. Some rate-limited endpoints add a code field (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 /health exist at the app root; /metrics is 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 bytesfilename 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.