Gold rates
Every rate in GOLD PRO is a price in rupees per gram. Nothing in the system is denominated per 10 grams, and the validation described on this page exists largely to catch a per-10-gram figure entered into a per-gram field.
Two kinds of rate coexist. The market rate is a universal figure with 24K, 22K
and 18K values, either fetched live or entered by a tenant admin. A per-bank
rate is the rate one partner bank publishes for gold loans, which trails the
market and is a privately negotiated commercial term. A valuation is priced at one
or the other, chosen per valuation through rateBasis. See
Gold rates for the operational side.
How a rate is resolved
When a valuation is created, or when you read the latest rate, the server resolves the applicable market rate in three tiers and stops at the first hit.
- The tenant's own newest rate row. Any rate recorded against your tenant, regardless of how it was set.
- The newest online rate across the whole platform. This lookup is not scoped to a tenant. Only rows fetched from the live market qualify; a rate a tenant typed by hand is never shared, so one shop's figure can never price another shop's valuation.
- A live fetch, performed on demand. Used at cold start when nothing is stored anywhere. The result is returned but not saved.
The resolved value carries the rates, a timestamp and an origin of "tenant",
"universal" or "live" — never another tenant's row identity.
Resolution fails, and returns nothing, only when no online rate exists anywhere on
the platform and all three live providers fail. That is the one case where
POST /api/valuations returns No gold rate available. Please try again shortly.
Tier 1 has a consequence worth planning for: once your tenant has recorded any rate at all, it stops falling back to the universal one. A manual rate pins your tenant to that figure until a newer row is appended — by another manual entry, or by the auto-sync job if you have it enabled. A brand-new tenant with no rows of its own tracks the platform's live rate automatically.
The live fetch tries GoldAPI.io first when GOLDAPI_API_KEY is configured, then
goldprice.org, then CoinGecko. All three report international spot, so each is
scaled by a domestic premium — import duty, GST and dealer margin — read from
GOLD_DOMESTIC_PREMIUM and defaulting to 1.128. That gap moves with real-world
duty and GST changes; it is an approximation of the Indian market rate, not a quote
from it.
Read the applicable rate
GET /api/admin/gold-rates/latest
Returns the market rate per gram that a valuation created right now would be priced
at, resolved through the three tiers above. Takes no parameters. Requires a staff
principal — TENANT_ADMIN or TENANT_EMPLOYEE.
curl https://goldpro.shop/api/admin/gold-rates/latest \
-H "Authorization: Bearer $TOKEN"
{
"rates": {
"24K": 9990,
"22K": 9150,
"20K": 8325,
"18K": 7493
},
"lastUpdated": "2026-07-16T06:00:11.402Z",
"source": "tenant"
}
| Field | Type | Description |
|---|---|---|
rates | object | Rupees per gram, keyed by karat. |
lastUpdated | string or null | When the resolved rate was recorded, as an ISO timestamp. For a live on-demand fetch this is the moment of the fetch. |
source | string | Which tier answered: "tenant", "universal", "live", or "none". |
When resolution fails entirely the endpoint still returns 200, with every rate
zeroed:
{
"rates": { "24K": 0, "22K": 0, "20K": 0, "18K": 0 },
"lastUpdated": null,
"source": "none"
}
Check source rather than the status code. A source of "none" means
POST /api/valuations will currently reject a valuation with
No gold rate available. Please try again shortly., and zeroed rates are a sentinel
for that condition, not a real price.
This endpoint reports a
20Krate per gram, computed as the 24K rate multiplied by 20/24. Nothing else in the system accepts it.POST /api/valuationsrejects a 20K ornament outright withUnsupported purity "20K". Supported values are 24K, 22K, 18K.Do not offer 20K as a selectable purity, and do not price against this key. The valuation engine carries per-gram rates for 24K, 22K and 18K only.
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
Record a market rate
POST /api/admin/gold-rates
Records your tenant's own market rate. Requires TENANT_ADMIN; a
TENANT_EMPLOYEE receives 401 even though they can read the rate above.
| Name | Type | Required | Description |
|---|---|---|---|
rate24k | number | Yes | Rupees per gram for 24K. |
rate22k | number | Yes | Rupees per gram for 22K. |
rate18k | number | Yes | Rupees per gram for 18K. |
All three are required and validated together. A missing or non-numeric field
becomes NaN and fails the positive-value check.
This appends a row rather than replacing one. Previous rates are retained and the newest wins on read, so the history of what you charged remains queryable. The row is written with a manual source, which makes it yours alone: it is never offered to another tenant as a fallback, and, per tier 1 above, it stops your tenant falling back to the platform rate.
curl -X POST https://goldpro.shop/api/admin/gold-rates \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "rate24k": 9990, "rate22k": 9150, "rate18k": 7493 }'
{
"success": true,
"rates": {
"rate24k": 9990,
"rate22k": 9150,
"rate18k": 7493,
"createdAt": "2026-07-16T06:00:11.402Z"
}
}
The change is recorded in the audit log. Existing valuations are unaffected: each one snapshots the rates it was priced at, so a certificate reprinted after a rate change still shows the figures that produced it.
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a TENANT_ADMIN. |
| 400 | Gold rates must be greater than zero. | Any rate is zero, negative, missing or non-numeric. |
| 400 | Rates should follow 24K >= 22K >= 18K. | The purity ordering is inverted. |
| 400 | A band message | A rate is implausible for its 24K. See below. |
| 500 | Internal Server Error | Any other failure. |
Rate validation
Every writer — the endpoint above, the admin screen, and the automated live sync — passes through one validator, so a rate the sync would write can never be one a human would be blocked from saving.
| Rule | Requirement |
|---|---|
| Positive | All three rates must be greater than zero. |
| Ordering | 24K >= 22K >= 18K. |
| 22K band | Between 82% and 98% of the 24K rate. Pure 22K is about 91.6%. |
| 18K band | Between 65% and 85% of the 24K rate. Pure 18K is 75%. |
Rates are rounded to two decimals on write. Ordering alone would let a units mistake
through — a per-10-gram 24K of 123112 sits happily above a per-gram 22K of
12322 — so the bands are what actually catch it. A failure names the expected
figure:
{
"error": "22K rate (₹12322) looks wrong for a 24K of ₹123112. Expected roughly ₹112771/g. Check the rates are all per-gram."
}
The live-fetch paths add their own guard, rejecting any 24K outside ₹1,000 to ₹100,000 per gram before the shared validator runs, so a compromised or broken upstream price cannot be written verbatim and then price every tenant's valuations.
Per-bank rates
A bank publishes a single figure for gold loans: the 22K rate per gram. That is all the system stores for a bank. The 24K and 18K values are derived on read using the same fixed purity ratios as everywhere else: 24K is the 22K divided by 0.916, and 18K is 75% of that resulting 24K. A bank therefore has one source of truth, and its karats cannot drift apart.
There is exactly one current rate per tenant per bank. An edit overwrites it and is
written to the audit log with the before and after figures. There is no history
table and none is needed: every valuation snapshots liveGoldRate22k and
bankGoldRate22k as they stood when it was priced, so past certificates stay
reproducible after a rate changes.
A per-bank rate has no cross-tenant fallback of any kind. Unlike the market rate it is a privately negotiated commercial term, not a shared market figure, and one shop's rate for a bank is never visible to another.
To price a valuation at a bank's rate, send both a bankId and rateBasis: "bank"
to POST /api/valuations. The bank's rate is looked up
whether or not you ask for it, because the certificate prints the live and bank
rates side by side regardless of which one applied. Asking for the bank basis when
no rate is configured fails rather than silently repricing at the live rate:
{
"error": "The selected bank has no gold rate configured. Set a rate for this bank, or value at the live rate."
}
Clearing a bank's rate makes subsequent valuations for that bank fall back to the live market rate. Valuations already made keep their snapshot.
There is no REST endpoint for reading or writing per-bank rates. They are managed only through the
/admin/banksscreen, which calls server actions restricted toTENANT_ADMIN. An integrator can select the bank basis on a valuation but cannot configure the rate over HTTP.
Input for a bank rate is a single 22K figure, validated on write: it must be greater
than zero and between ₹500 and ₹100,000 per gram. Outside that range you get
₹108550 is outside the plausible range for a 22K rate. Enter the rate per gram, not per 10 grams. The derived 24K and 18K then pass the shared validator by
construction.
List banks
GET /api/admin/banks
Returns the banks your tenant can attach to a valuation: your own bank templates
plus the shared platform catalog, active ones only, ordered by name. Requires
TENANT_ADMIN.
Use the ids returned here as bankId when creating a valuation.
curl https://goldpro.shop/api/admin/banks \
-H "Authorization: Bearer $TOKEN"
[
{ "id": "clx8jy0k20001qw3fa1b2c3d4", "name": "Bank of Maharashtra" },
{ "id": "clx8jy0k20002qw3fe5f6g7h8", "name": "HDFC Bank" },
{ "id": "clx8jy0k20003qw3fi9j0k1l2", "name": "State Bank of India" }
]
Only id and name are returned. The list is de-duplicated case-insensitively
after common suffixes are stripped, so "State Bank of India" and "State Bank of
India Ltd" collapse to a single entry — the first in name order — even when both a
shared template and your own template exist. A tenant cannot see or attach another
tenant's private bank template.
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a TENANT_ADMIN. |
| 500 | The underlying error message | Query failure. This route returns the thrown message rather than a generic string. |
Related
- Gold rates — auto-sync, and which rate a valuation is priced at.
- Banks and branches — partner banks and the managers reports go to.
- Valuations —
rateBasis, and the snapshots a valuation keeps.