API overview

GOLD PRO exposes one HTTP API. The web app, the three native apps, and anything you build against the platform all call the same routes — there is no separate integration API with its own contract. What is documented here is what the product itself runs on.

You never pass a tenant id. Every route derives the tenant from the credentials you present and scopes its queries to it, so a request can only ever read or write inside one shop. See Authentication for how a caller is resolved into a principal.

Base URL

All examples in this reference use:

https://goldpro.shop

www.goldpro.shop is equivalent, and the deployment host (gold-valuation.vercel.app) and localhost:3000 in development are treated the same way. These are the main domains.

The customer portal is served from a per-tenant subdomain instead. A request to <slug>.goldpro.shop/dashboard is rewritten internally to /tenant-portal/<slug>/dashboard. The slug is taken from the Host header, which a caller controls, so it is validated before it is trusted to select a tenant context; a malformed, reserved, or unknown slug gets a plain-text 404 rather than a rewrite.

On a tenant subdomain, every /api path is passed straight through the edge with no session check — each handler's own authentication is the only gate. This is deliberate: portal customers hold a customer_token cookie, which is not a NextAuth session and would fail the edge rule that guards the main domain. Build integrations against the main domain, where both layers apply.

Requests and responses

Send JSON with Content-Type: application/json. Responses are JSON unless noted below, and every failure uses the envelope described in Errors.

ConcernBehaviour
Request bodiesJSON, except POST /api/upload and POST /api/ocr/parse, which take multipart/form-data
File uploadPOST /api/upload with a single file part. Returns { "pathname": "..." } — a private blob path, not a public URL
File readGET /api/blob?pathname=... streams the stored file under its original content type
PDFsGET /api/billing/documents/{id}/pdf responds application/pdf
TimestampsISO 8601 strings
VersioningNone. There is no version prefix and no version header

Blob pathnames are namespaced by tenant (<tenantId>/...) and /api/blob refuses any pathname whose first segment is not your own tenant. Customers cannot read blobs at all, because the namespace is per-tenant rather than per-customer.

The two mutation surfaces

GOLD PRO writes through two distinct mechanisms. Knowing which one you are looking at matters, because only one of them is reachable from outside a browser.

REST routes

Everything under /api/*. These are what the native apps, the offline sync queue, and your integrations call. They accept either a session cookie or a Bearer token, resolved through one shared helper.

Server actions

The web UI performs most of its writes through Next.js server actions rather than fetch. There are 44, across seven files in src/actions/:

FileActionsGuard
admin.actions.ts29requireTenantAdmin (26), requireTenantAccess (3)
customer.actions.ts7requireTenantAccess
tenant.actions.ts3super-admin sa_token cookie
valuation.actions.ts2requireTenantAdmin, requireTenantAccess
communication.actions.ts1requireTenantAccess
emi.actions.ts1requireTenantAccess
upload.actions.ts1requireTenantAccess

Server actions are browser-only. requireTenantAccess and requireTenantAdmin resolve the caller through getServerSession, which reads the NextAuth staff session cookie, and the super-admin actions read the sa_token cookie directly. None of them inspect the Authorization header, so a Bearer token cannot invoke one — which is why the native apps never call them. They are an internal surface with no stable contract.

If you are integrating, use the REST routes.

Valuation creation is REST-only

There is deliberately no valuation-creating server action. POST /api/valuations is the sole creation path, from every client. An earlier createValuation action persisted client-supplied figures verbatim and was removed to close that gap: the REST handler recomputes gross, net, and total value from the tenant's stored gold rate, clamps LTV server-side, and rejects any purity outside 24K, 22K and 18K. A client asks for a rate basis; the server decides what the valuation is worth.

See Valuations for the full contract.

Web and mobile share one API

/api/mobile/* looks like a parallel surface for native clients. It is not. It covers native sign-in plus a small set of screens with no direct web equivalent:

RouteMethods
/api/mobile/auth/staff/loginPOST
/api/mobile/auth/customer/loginPOST
/api/mobile/auth/superadmin/loginPOST
/api/mobile/auth/meGET
/api/mobile/auth/session-statusGET
/api/mobile/auth/logoutPOST
/api/mobile/devicesPOST, DELETE
/api/mobile/personalizationGET, PATCH
/api/mobile/personalization/display-namePATCH
/api/mobile/personalization/logoPOST, DELETE
/api/mobile/personalization/notificationsPATCH
/api/mobile/personalization/tax-invoicePATCH
/api/mobile/personalization/change-requestsGET, POST
/api/mobile/bank-branchesGET, POST, PATCH, DELETE
/api/mobile/subscriptionGET

For everything else — valuations, customers, EMI, upload, OCR, tickets, notifications, dashboard, billing, and the customer portal — native clients call the same routes the web calls, sending Authorization: Bearer $TOKEN instead of a cookie:

curl https://goldpro.shop/api/customers \
  -H "Authorization: Bearer $TOKEN"

One route, one handler, one set of rules, whether the caller is a browser or an app. There is no second implementation to drift out of step.

The surface at a glance

NamespacePurposePrincipal
/api/valuationsCreate a valuation; read, update, report, dispatchStaff
/api/customersCustomer book and per-customer timelinesStaff
/api/admin/*Gold rates, banks, invoices, templatesTenant admin
/api/emiSchedules and pending countsStaff
/api/upload, /api/blob, /api/ocr/parseMedia capture, private file reads, receipt OCRStaff
/api/notifications, /api/dashboard/summaryIn-app notices, dashboard figuresStaff
/api/tickets/*Support ticketsStaff, customer, super admin
/api/billing/*Plans, quotes, checkout, purchases, documentsTenant admin
/api/portal/*A customer's own valuations, EMIs, timeline, notificationsCustomer
/api/tenant-portal/*Customer email-OTP login and logoutPublic
/api/system-gate/*Super-admin login and logoutPublic
/api/super-admin/*Platform operations across all tenantsSuper admin
/api/mobile/*Native sign-in and native-only screensAll
/api/customer-intake, /api/otp/*Self-service intake behind a signed linkPublic
/api/contactAccess-request formPublic
/api/onboarding/*Checklist and product-tour stateStaff
/api/cron/*Scheduled jobsCRON_SECRET
/api/razorpay/webhookPayment eventsHMAC signature

"Public" means the route authenticates itself in the handler — with a signed intake token, an OTP, or nothing at all — rather than requiring a session at the edge. It does not mean unauthenticated.

Automated callers

Two callers are not people, and neither uses the principal model.

/api/cron/* is authenticated by a CRON_SECRET Bearer and fails closed: if the secret is unset the route returns 401 rather than running. Schedules live in vercel.json:

PathSchedule (UTC)
/api/cron/emi-reminders30 4 * * *
/api/cron/sync-rates0 */8 * * *
/api/cron/billing-reconcile15 * * * *

/api/razorpay/webhook is authenticated by an HMAC over the raw request body, which is why the body is read as text and only parsed once the signature passes. It is the source of truth for whether a payment happened — the browser callback at /api/billing/verify is a convenience so the tenant sees their plan immediately. See Webhooks.

How to read this reference

Each endpoint page lists routes with their methods, a parameter table, a runnable request, and a real response body. Parameter tables use the columns Name, Type, Required, Description. Secrets appear as $TOKEN.

Start with Authentication — every other page assumes you can produce a credential — then Errors, which covers the envelope, rate limits, and the idempotency key that makes retries safe.

One thing to know before you read allowed values anywhere in this reference: there are no database enums. Every status field in the data model is a plain String, with its permitted values recorded in a comment beside it in prisma/schema.prisma. Where this reference lists allowed values, they are transcribed by hand from those comments. Treat the handler as the real authority on what it accepts, and do not assume the database will reject an unexpected string.