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.

NameTypeRequiredDescription
customerIdstringYesId of a customer in your tenant, or the literal "Other" to create one inline.
ornamentsarrayYesAt least one ornament object. See the table below.
typestringNo"loan", "sell" or "certificate". Any other value is stored as "loan".
bankIdstringNoA bank template id from the shared catalog or your own, or "Other" with customBankName. Absent, empty or non-string is treated as no bank.
rateBasisstringNo"live" (default) or "bank". Only the exact string "bank" selects the bank basis.
valuationFeenumberNoClamped to zero or more. Defaults to 0.
ltvPercentagenumberNoLoan only. Clamped to the range 0–100; blank or unparseable becomes 75. An explicit 0 is honoured.
interestRatenumberNoLoan only. Annual percentage. Unparseable becomes null.
requestedLoanAmountnumberNoLoan only. When greater than zero, the estimated loan is the lower of this and Total Value × LTV.
emiInstallmentsnumberNoLoan only. Repayment months, capped at 60. Blank or 0 produces a single bullet installment.
customCustomerNamestringConditionalRequired when customerId is "Other". 2–50 characters; letters, spaces, dots, hyphens and apostrophes.
customCustomerPhonestringConditionalRequired when customerId is "Other". Normalized to a canonical 10-digit Indian mobile number before storage.
customBankNamestringConditionalRequired when bankId is "Other".
ornamentReceiptUrlstringNoPurchase receipt for the ornaments. Blank strings become null.
appraiserNamestringNoDefaults to the authenticated user's name.
appraiserNotestringNoFree text.
clientRequestIdstringNoIdempotency key. See Idempotency below.
sendToCustomerbooleanNoWrites a dispatch log line only. No message is sent.
sendToBankbooleanNoWrites a dispatch log line only. No message is sent.

sendToCustomer and sendToBank do 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:

NameTypeRequiredDescription
typestringYesItem 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".
puritystringYes"24K", "22K" or "18K". No other value is priced.
grossWeightnumberYesGrams. Must be greater than zero. Unparseable becomes 0, which then fails.
deductionnumberNoGrams of stone or wax to subtract. Defaults to 0.
quantityintegerNoNumber of identical pieces weighed together. Defaults to 1. The weights above are the set's total and are not multiplied by it.
imageUrlstringNoPhoto of the item.
imageUrl2stringNoSecond photo.
purityMethodstringNoFor example "Touchstone", "XRF", "Acid".
purityVerifiedbooleanNoOnly 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 stored type falls back to "loan" for any unrecognised or omitted value, but ltvPercentage, interestRate, estimatedLoan and the EMI schedule are all computed from the value you sent, not the stored fallback. Omitting type therefore stores a loan with a null LTV, a null interest rate, an estimated loan of 0 and 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

StatusErrorCondition
401UnauthorizedNot a staff principal.
400Missing required fields or valid ornamentscustomerId absent, or ornaments missing, not an array, or empty.
400Invalid customercustomerId is not "Other" and no customer with that id exists in your tenant.
400A validation messagecustomerId is "Other" with a name or phone that fails validation, for example Phone number must be a valid 10-digit Indian mobile number.
409A 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.
400Missing name for new bankbankId is "Other" without customBankName.
400Invalid bank templatebankId is not an active template in your tenant or the shared catalog.
400No gold rate available. Please try again shortly.No stored rate exists anywhere and every live provider failed. See Gold rates.
400The 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.
400Unsupported purity "20K". Supported values are 24K, 22K, 18K.An ornament purity outside the three supported values.
400Gross weight must be greater than zero.An ornament's gross weight is zero, negative or unparseable.
400Net weight cannot exceed gross weight.A negative deduction pushes net above gross.
400Quantity must be a whole number of at least 1.An ornament's quantity is fractional, zero or negative.
409A 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.
500Internal Server ErrorAn 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.

StatusErrorCondition
401UnauthorizedNot a staff principal.
404Valuation not foundNo 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.

NameTypeRequiredDescription
goldEnvelopeUrlstringNoPhoto of the sealed gold loan envelope.
acknowledgementUrlstringNoScan of the customer acknowledgement with stamp and signature.
appraiserNamestringNoWho performed the valuation.
appraiserNotestringNoFree 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."
}
StatusErrorCondition
401UnauthorizedNot a staff principal.
404Valuation not foundNo such id in your tenant.
400No supported fields to updateNone 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".

NameTypeRequiredDescription
amountnumberYesMust parse to a finite number greater than zero.
dueDatestringYesAny 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"
}
StatusErrorCondition
401UnauthorizedNot a staff principal.
400Invalid amount or due dateamount is not finite or is zero or less, or dueDate does not parse.
404Valuation not foundNo such id in your tenant.

Update an installment status

PATCH /api/valuations/{id}/emi

Requires a staff principal.

NameTypeRequiredDescription
emiIdstringYesMust belong to this valuation and your tenant.
statusstringYesOne 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"
}
StatusErrorCondition
401UnauthorizedNot a staff principal.
400Missing EMI ID or statusEither field is absent or falsy.
400Invalid EMI statusstatus is outside the four allowed values.
404EMI record not foundThe 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 }
StatusErrorCondition
401UnauthorizedNot a staff principal.
400Valuation not found or customer has no emailNo such id in your tenant, or the customer record has no email address.
500Internal Server ErrorPDF generation or the email provider failed.

A missing valuation and a customer without an email are indistinguishable here: both return 400 with the same message. This endpoint does not return 404.

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"
}
StatusErrorCondition
401UnauthorizedNot a staff principal.
404Valuation not foundNo 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.

NameTypeRequiredDescription
customerEmailbooleanNoEmails the PDF to the customer. Only the exact value true enables it.
customerWhatsAppbooleanNoSends a WhatsApp message to the customer's phone. Only the exact value true enables it.
bankEmailbooleanNoEmails the PDF to the bank recipient. Only the exact value true enables it.
bankBranchIdstringNoThe 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.

StatusErrorCondition
401UnauthorizedNot a staff principal.
404Valuation not found.No such id in your tenant.
502Body is the sent/skipped/failed breakdownEvery attempted target failed and none delivered.
500Internal Server ErrorPDF 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.

  • 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.