Valuations
A valuation prices a set of ornaments against a gold rate per gram and produces the figures a certificate is rendered from. It is the central object in GOLD PRO: a customer owns it, ornaments belong to it, an EMI schedule hangs off it, and the report you send to a partner bank is generated from it.
Every endpoint on this page requires a staff principal — TENANT_ADMIN or
TENANT_EMPLOYEE — and is scoped to that principal's tenant. A valuation id
belonging to another tenant reads as not found rather than forbidden. See
Authentication for how a web session cookie and a
native Bearer token resolve to the same principal.
POST /api/valuations is the only path that creates a valuation. There is no
server-action equivalent; the previous one persisted client-supplied figures
verbatim and was removed to close that gap. The server recomputes every weight,
value and loan amount from the ornaments you send and the rate it resolves, so the
totals in the response are authoritative.
Create a valuation
POST /api/valuations
Prices the ornaments, resolves the applicable gold rate per gram, writes the valuation, and — for a loan — generates the EMI repayment schedule. Requires a staff principal.
Only two fields are mandatory: customerId and a non-empty ornaments array.
Everything else is optional and has a server-side default.
| Name | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Id of a customer in your tenant, or the literal "Other" to create one inline. |
ornaments | array | Yes | At least one ornament object. See the table below. |
type | string | No | "loan", "sell" or "certificate". Any other value is stored as "loan". |
bankId | string | No | A bank template id from the shared catalog or your own, or "Other" with customBankName. Absent, empty or non-string is treated as no bank. |
rateBasis | string | No | "live" (default) or "bank". Only the exact string "bank" selects the bank basis. |
valuationFee | number | No | Clamped to zero or more. Defaults to 0. |
ltvPercentage | number | No | Loan only. Clamped to the range 0–100; blank or unparseable becomes 75. An explicit 0 is honoured. |
interestRate | number | No | Loan only. Annual percentage. Unparseable becomes null. |
requestedLoanAmount | number | No | Loan only. When greater than zero, the estimated loan is the lower of this and Total Value × LTV. |
emiInstallments | number | No | Loan only. Repayment months, capped at 60. Blank or 0 produces a single bullet installment. |
customCustomerName | string | Conditional | Required when customerId is "Other". 2–50 characters; letters, spaces, dots, hyphens and apostrophes. |
customCustomerPhone | string | Conditional | Required when customerId is "Other". Normalized to a canonical 10-digit Indian mobile number before storage. |
customBankName | string | Conditional | Required when bankId is "Other". |
ornamentReceiptUrl | string | No | Purchase receipt for the ornaments. Blank strings become null. |
appraiserName | string | No | Defaults to the authenticated user's name. |
appraiserNote | string | No | Free text. |
clientRequestId | string | No | Idempotency key. See Idempotency below. |
sendToCustomer | boolean | No | Writes a dispatch log line only. No message is sent. |
sendToBank | boolean | No | Writes a dispatch log line only. No message is sent. |
sendToCustomerandsendToBankdo not deliver anything. They emit a server log line and nothing else. To actually send a report, call POST /api/valuations/[id]/send or POST /api/valuations/[id]/report.
Each entry in ornaments accepts:
| Name | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Item description, for example "Bangle". Coerced to a string and stored as sent; not checked against a list and not enforced — omitting it stores the literal text "undefined". |
purity | string | Yes | "24K", "22K" or "18K". No other value is priced. |
grossWeight | number | Yes | Grams. Must be greater than zero. Unparseable becomes 0, which then fails. |
deduction | number | No | Grams of stone or wax to subtract. Defaults to 0. |
quantity | integer | No | Number of identical pieces weighed together. Defaults to 1. The weights above are the set's total and are not multiplied by it. |
imageUrl | string | No | Photo of the item. |
imageUrl2 | string | No | Second photo. |
purityMethod | string | No | For example "Touchstone", "XRF", "Acid". |
purityVerified | boolean | No | Only the exact value true stores true. |
Net weight is derived as max(0, grossWeight - deduction) rounded to two decimals
and cannot be supplied. An item's estimated value is that same difference multiplied
by the applicable per-gram rate for its purity — taken unrounded, so it is not
exactly the stored two-decimal netWeight times the rate when the difference
carries more precision.
curl -X POST https://goldpro.shop/api/valuations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "clx8k2p9a0001qw3f7h2mz1yv",
"type": "loan",
"ltvPercentage": 75,
"interestRate": 12,
"emiInstallments": 12,
"valuationFee": 500,
"ornaments": [
{
"type": "Bangle",
"purity": "22K",
"quantity": 2,
"grossWeight": 24.5,
"deduction": 1.5,
"purityMethod": "Touchstone",
"purityVerified": true
}
]
}'
A successful call returns 201 with the stored valuation row. Ornaments are not
included in this response; read them back with
GET /api/valuations/[id].
{
"id": "clx8k9q1b0003qw3f4d8nc2ab",
"clientRequestId": null,
"customerId": "clx8k2p9a0001qw3f7h2mz1yv",
"tenantId": "clx8jz1r70000qw3f9k5te0pq",
"type": "loan",
"goldRate": 9150,
"rateBasis": "live",
"liveGoldRate22k": 9150,
"bankGoldRate22k": null,
"valuationFee": 500,
"totalGrossWeight": 24.5,
"totalNetWeight": 23,
"totalValue": 210450,
"estimatedLoan": 157837.5,
"ltvPercentage": 75,
"interestRate": 12,
"makingCharges": 0,
"otherDeductions": 0,
"status": "Completed",
"bankId": null,
"bankBranchId": null,
"ornamentReceiptUrl": null,
"appraiserName": "R. Kulkarni",
"appraiserNote": null,
"goldEnvelopeUrl": null,
"acknowledgementUrl": null,
"goldReceiptNo": null,
"goldReceiptIssuedAt": null,
"createdAt": "2026-07-16T09:14:22.104Z",
"updatedAt": "2026-07-16T09:14:22.104Z"
}
status is always "Completed" on creation. goldRate is the 22K rate per gram
that actually priced the ornaments, whichever basis won. liveGoldRate22k and
bankGoldRate22k snapshot both rates as they stood at creation so a certificate
re-rendered days later reproduces the same figures; bankGoldRate22k is null
when no bank rate applied.
Send
type: "loan"explicitly if you want loan behaviour. The storedtypefalls back to"loan"for any unrecognised or omitted value, butltvPercentage,interestRate,estimatedLoanand the EMI schedule are all computed from the value you sent, not the stored fallback. Omittingtypetherefore stores a loan with a null LTV, a null interest rate, an estimated loan of0and no schedule.
Creating a customer or bank inline
customerId: "Other" together with customCustomerName and customCustomerPhone
creates the borrower in the same transaction as the valuation. bankId: "Other"
together with customBankName creates a bank template the same way, unless a bank
with a matching name already exists for your tenant or in the shared catalog — in
which case the existing one is reused. Both are validated before anything is
written, so a rejected valuation never strands a borrower or a bank template.
A bank your tenant has never seen cannot have a rate configured, so pairing a
brand-new customBankName with rateBasis: "bank" fails.
bankBranchId is not accepted here. The branch a report is dispatched to is
chosen at send time — see
Dispatch to a customer and bank.
Idempotency
Pass a stable clientRequestId to make retries safe. If a valuation already exists
for that key within your tenant, the existing row is returned with 200 instead of
201 and nothing new is created. A concurrent retry that loses the insert race is
resolved the same way. This is what the offline sync queue in the native apps
relies on.
Errors
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
| 400 | Missing required fields or valid ornaments | customerId absent, or ornaments missing, not an array, or empty. |
| 400 | Invalid customer | customerId is not "Other" and no customer with that id exists in your tenant. |
| 400 | A validation message | customerId is "Other" with a name or phone that fails validation, for example Phone number must be a valid 10-digit Indian mobile number. |
| 409 | A borrower with phone 9876543210 already exists (Anita Deshmukh). Please select them from the borrower list instead of adding a new one. | customerId is "Other" and that phone already belongs to one of your customers. A separate wording is returned when the match is archived. |
| 400 | Missing name for new bank | bankId is "Other" without customBankName. |
| 400 | Invalid bank template | bankId is not an active template in your tenant or the shared catalog. |
| 400 | No gold rate available. Please try again shortly. | No stored rate exists anywhere and every live provider failed. See Gold rates. |
| 400 | The selected bank has no gold rate configured. Set a rate for this bank, or value at the live rate. | rateBasis is "bank" and no rate is configured for that bank. |
| 400 | Unsupported purity "20K". Supported values are 24K, 22K, 18K. | An ornament purity outside the three supported values. |
| 400 | Gross weight must be greater than zero. | An ornament's gross weight is zero, negative or unparseable. |
| 400 | Net weight cannot exceed gross weight. | A negative deduction pushes net above gross. |
| 400 | Quantity must be a whole number of at least 1. | An ornament's quantity is fractional, zero or negative. |
| 409 | A borrower with this phone number already exists. Please select them from the borrower list instead of adding a new one. | The unique phone constraint fired after the pre-check, under concurrency. |
| 500 | Internal Server Error | An actual fault on our side. Every rejection of what you sent is a 400 with the reason in error. |
A rateBasis of "bank" fails rather than silently repricing at the live rate.
This matters for stale requests: if the bank's rate was removed after your form
loaded, or an offline valuation syncs long after capture, repricing would return a
total the appraiser never agreed to.
Retrieve a valuation
GET /api/valuations/{id}
Returns the full valuation detail: ornament breakdown, evidence URLs, gold-receipt state, EMI schedule, the owning customer, and the branches available for dispatch. Requires a staff principal.
curl https://goldpro.shop/api/valuations/clx8k9q1b0003qw3f4d8nc2ab \
-H "Authorization: Bearer $TOKEN"
{
"id": "clx8k9q1b0003qw3f4d8nc2ab",
"type": "loan",
"status": "Completed",
"totalGrossWeight": 24.5,
"totalNetWeight": 23,
"totalValue": 210450,
"estimatedLoan": 157837.5,
"valuationFee": 500,
"makingCharges": 0,
"otherDeductions": 0,
"goldRate": 9150,
"ltvPercentage": 75,
"interestRate": 12,
"appraiserName": "R. Kulkarni",
"appraiserNote": null,
"ornamentReceiptUrl": null,
"goldEnvelopeUrl": null,
"acknowledgementUrl": null,
"goldReceiptNo": null,
"goldReceiptIssuedAt": null,
"createdAt": "2026-07-16T09:14:22.104Z",
"updatedAt": "2026-07-16T09:14:22.104Z",
"customer": {
"id": "clx8k2p9a0001qw3f7h2mz1yv",
"name": "Anita Deshmukh",
"phone": "9876543210",
"email": "anita@example.com"
},
"bank": null,
"bankBranchId": null,
"bankBranches": [],
"ornaments": [
{
"id": "clx8k9q1c0004qw3f1p6ba9de",
"type": "Bangle",
"quantity": 2,
"purity": "22K",
"grossWeight": 24.5,
"netWeight": 23,
"deduction": 1.5,
"estimatedValue": 210450,
"imageUrl": null,
"imageUrl2": null,
"purityMethod": "Touchstone",
"purityVerified": true
}
],
"emiRecords": [
{
"id": "clx8k9q1d0005qw3f8m2xt7fg",
"amount": 14773.17,
"dueDate": "2026-08-15T09:14:22.104Z",
"status": "Upcoming",
"paidAt": null,
"valuationId": "clx8k9q1b0003qw3f4d8nc2ab"
}
]
}
bank is { "id", "name" } when a bank is attached, otherwise null.
bankBranches lists your tenant's active branches for that bank, each as
{ "id", "branchName", "ifsc", "managerEmail", "managerName" }; it is an empty
array when the valuation has no bank. Ornaments are ordered by id, EMI records by
due date. Note that rateBasis, liveGoldRate22k and bankGoldRate22k are not
part of this projection even though they are stored on the row.
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
| 404 | Valuation not found | No such id in your tenant. |
Update valuation evidence
PATCH /api/valuations/{id}
Updates the post-valuation evidence captured after the ornaments are sealed. Only four fields can be written; everything else on a valuation is immutable through this API. Requires a staff principal.
| Name | Type | Required | Description |
|---|---|---|---|
goldEnvelopeUrl | string | No | Photo of the sealed gold loan envelope. |
acknowledgementUrl | string | No | Scan of the customer acknowledgement with stamp and signature. |
appraiserName | string | No | Who performed the valuation. |
appraiserNote | string | No | Free text. |
A field is written only when the key is present in the body. Sending an empty
string, a blank string or null clears the field to null. At least one of the
four keys must be present.
curl -X PATCH https://goldpro.shop/api/valuations/clx8k9q1b0003qw3f4d8nc2ab \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"goldEnvelopeUrl": "https://blob.goldpro.shop/envelopes/clx8k9q1b.jpg",
"appraiserNote": "Two bangles, hallmarked, sealed in envelope 41."
}'
{
"success": true,
"goldEnvelopeUrl": "https://blob.goldpro.shop/envelopes/clx8k9q1b.jpg",
"acknowledgementUrl": null,
"appraiserName": "R. Kulkarni",
"appraiserNote": "Two bangles, hallmarked, sealed in envelope 41."
}
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
| 404 | Valuation not found | No such id in your tenant. |
| 400 | No supported fields to update | None of the four keys were present in the body. |
Manage the EMI schedule
A loan valuation generates its schedule at creation. These two endpoints let you append a custom installment and move an installment between states. For reading schedules across valuations, see EMI.
Add an installment
POST /api/valuations/{id}/emi
Requires a staff principal. The installment inherits the valuation's customer and
tenant, and is created with status "Upcoming".
| Name | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Must parse to a finite number greater than zero. |
dueDate | string | Yes | Any value new Date() parses, for example an ISO 8601 timestamp. |
curl -X POST https://goldpro.shop/api/valuations/clx8k9q1b0003qw3f4d8nc2ab/emi \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "amount": 14773.17, "dueDate": "2027-08-15T00:00:00.000Z" }'
{
"id": "clx8kb3f70009qw3f2v4rq8hd",
"customerId": "clx8k2p9a0001qw3f7h2mz1yv",
"valuationId": "clx8k9q1b0003qw3f4d8nc2ab",
"tenantId": "clx8jz1r70000qw3f9k5te0pq",
"amount": 14773.17,
"dueDate": "2027-08-15T00:00:00.000Z",
"status": "Upcoming",
"isRead": false,
"reminderSentAt": null,
"todayReminderSentAt": null,
"overdueReminderSentAt": null,
"paidAt": null,
"createdAt": "2026-07-16T09:31:02.551Z",
"updatedAt": "2026-07-16T09:31:02.551Z"
}
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
| 400 | Invalid amount or due date | amount is not finite or is zero or less, or dueDate does not parse. |
| 404 | Valuation not found | No such id in your tenant. |
Update an installment status
PATCH /api/valuations/{id}/emi
Requires a staff principal.
| Name | Type | Required | Description |
|---|---|---|---|
emiId | string | Yes | Must belong to this valuation and your tenant. |
status | string | Yes | One of "Paid", "Due Soon", "Overdue", "Upcoming". |
Moving an installment to "Paid" stamps paidAt with the current time, unless it
already carries one. Moving it out of "Paid" clears paidAt back to null. A
fresh transition into "Paid" also notifies the customer by push and email on a
best-effort basis; a notification failure does not fail the request.
curl -X PATCH https://goldpro.shop/api/valuations/clx8k9q1b0003qw3f4d8nc2ab/emi \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "emiId": "clx8kb3f70009qw3f2v4rq8hd", "status": "Paid" }'
{
"id": "clx8kb3f70009qw3f2v4rq8hd",
"customerId": "clx8k2p9a0001qw3f7h2mz1yv",
"valuationId": "clx8k9q1b0003qw3f4d8nc2ab",
"tenantId": "clx8jz1r70000qw3f9k5te0pq",
"amount": 14773.17,
"dueDate": "2027-08-15T00:00:00.000Z",
"status": "Paid",
"isRead": false,
"reminderSentAt": null,
"todayReminderSentAt": null,
"overdueReminderSentAt": null,
"paidAt": "2026-07-16T09:35:47.210Z",
"createdAt": "2026-07-16T09:31:02.551Z",
"updatedAt": "2026-07-16T09:35:47.212Z"
}
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
| 400 | Missing EMI ID or status | Either field is absent or falsy. |
| 400 | Invalid EMI status | status is outside the four allowed values. |
| 404 | EMI record not found | The id does not match this valuation and tenant. |
Email the report
POST /api/valuations/{id}/report
Renders the valuation certificate as a PDF and emails it to the customer. Takes no request body. Requires a staff principal.
The subject and body come from your tenant's EMAIL_REPORT communication template
when one exists, with {{customerName}}, {{tenantName}}, {{valuationId}},
{{totalValue}}, {{estimatedValue}}, {{estimatedLoan}}, {{emiAmount}},
{{dueDate}}, {{borrowerName}} and {{paymentLink}} interpolated and
HTML-escaped. Without a template, a default subject and the provider's standard
body are used. See Personalization.
curl -X POST https://goldpro.shop/api/valuations/clx8k9q1b0003qw3f4d8nc2ab/report \
-H "Authorization: Bearer $TOKEN"
{ "success": true }
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
| 400 | Valuation not found or customer has no email | No such id in your tenant, or the customer record has no email address. |
| 500 | Internal Server Error | PDF generation or the email provider failed. |
A missing valuation and a customer without an email are indistinguishable here: both return
400with the same message. This endpoint does not return404.
Issue a gold receipt
POST /api/valuations/{id}/gold-receipt
Assigns the gold receipt number for a valuation — the final step of the workflow, acknowledging that you hold the customer's gold. Takes no request body. Requires a staff principal.
The call is idempotent. The number and its issue date are assigned once and reused
on every later call, so this doubles as the read endpoint for receipt state. The
number is derived as GR- followed by the last six characters of the valuation id
in uppercase.
curl -X POST https://goldpro.shop/api/valuations/clx8k9q1b0003qw3f4d8nc2ab/gold-receipt \
-H "Authorization: Bearer $TOKEN"
{
"goldReceiptNo": "GR-8NC2AB",
"goldReceiptIssuedAt": "2026-07-16T09:41:18.882Z"
}
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
| 404 | Valuation not found | No such id in your tenant. |
Dispatch to a customer and bank
POST /api/valuations/{id}/send
Sends the report to any combination of the customer's email, the customer's WhatsApp, and the bank branch manager's email. Each target is attempted independently and reported separately. Requires a staff principal.
| Name | Type | Required | Description |
|---|---|---|---|
customerEmail | boolean | No | Emails the PDF to the customer. Only the exact value true enables it. |
customerWhatsApp | boolean | No | Sends a WhatsApp message to the customer's phone. Only the exact value true enables it. |
bankEmail | boolean | No | Emails the PDF to the bank recipient. Only the exact value true enables it. |
bankBranchId | string | No | The branch whose manager receives the bank copy. Validated against your tenant and the valuation's bank, then persisted onto the valuation for re-sends. Falls back to the valuation's stored branch. |
An unparseable body is treated as empty, which enables no targets and returns three empty arrays. EMI statuses are synchronised before the report is rendered, so the PDF reflects current due and overdue states.
The bank recipient resolves in order: the chosen branch's manager email, then the
bank template's default email. Subject and body resolve as the branch's override,
then your tenant's EMAIL_BANK template, then the shared template default. See
Banks and branches.
curl -X POST https://goldpro.shop/api/valuations/clx8k9q1b0003qw3f4d8nc2ab/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerEmail": true,
"bankEmail": true,
"bankBranchId": "clx8ka2d10007qw3f5j9wv3cz"
}'
{
"sent": ["customerEmail"],
"skipped": [],
"failed": [
{ "target": "bankEmail", "error": "Recipient address rejected." }
]
}
sent lists targets that delivered. skipped lists targets that could not be
attempted: customerEmail when the customer has no email address, bankEmail when
no recipient resolved. failed carries the provider error per target.
customerWhatsApp is always attempted and so never appears in skipped.
Retry only the targets in failed. Retrying the whole request re-sends the ones
that already succeeded — the customer receives the same PDF twice.
| Status | Error | Condition |
|---|---|---|
| 401 | Unauthorized | Not a staff principal. |
| 404 | Valuation not found. | No such id in your tenant. |
| 502 | Body is the sent/skipped/failed breakdown | Every attempted target failed and none delivered. |
| 500 | Internal Server Error | PDF generation or another unexpected failure. |
A partial failure returns 200 with the breakdown, not an error status. Check
failed on every response rather than relying on the status code alone. See
Errors.
Related
- Running a valuation — the workflow this API backs.
- Gold rates — how the applicable rate per gram is resolved.
- Customers — creating the borrower before you value for them.
- EMI — reading schedules across valuations.