Partner API · version 1

Offer gold and silver in your app

Lumos lets your customers buy, hold and sell physical gold and silver, fully backed by metal held in a secure vault in the UAE. You keep the customer and the experience. Lumos handles prices, the metal, the records and the reconciliation.

REST · JSONHMAC-SHA256 signingAED · grams

How it works

Your server talks to Lumos. Your mobile app talks only to your server and never holds a Lumos secret. A typical buy is three calls: get a quote, place it, and confirm payment. Lumos then records the metal against your customer and tells your server about every change by webhook.

You build

Your screens, customer onboarding and KYC, taking your customers' payments and paying their sells, and a webhook endpoint.

Lumos provides

Live prices, quotes, metal allocation, the ledger, holdings, invoices, daily settlement statements and webhooks.

Coming next

A hosted page you can open from your app for buy and sell screens, and a mobile SDK. Both use the same account and keys.

How money moves

Money moves one way only: through you. Your customer pays you for a buy, and you pay your customer for a sell. A customer never pays Lumos and is never paid by Lumos.

StepWhoWhat happens
BuyYou and your customerYou take the payment in your own wallet, card or bank flow, then complete the buy with your paymentReference.
SellYou and your customerYou pay your customer, then complete the sell with your paymentReference.
SettlementYou and LumosLumos sends a statement every day. The net of buys and sells is paid between you and Lumos, in whichever direction it falls, on the schedule agreed for your account. It can be settled by you or by a company that settles for you.

Getting started

  1. Get your keysLumos sends you a clientId and a clientSecret for the sandbox, through a secure channel. Keep the secret on your server only. Production keys follow once you pass the go-live checklist.
  2. Share your webhook URLAn HTTPS endpoint on your server that accepts POST requests. You can change it later.
  3. Make a signed callCall GET /v1/partner. A 200 with your settings means your signing works. The Postman collection does this for you.
  4. Run the core sequenceCreate a customer, read prices, preview and place a buy, confirm it, then preview and place a sell. See Buying and Selling.
  5. Handle webhooks and errorsVerify webhook signatures, de-duplicate events, and handle every error in Errors.
  6. Go liveWork through the go-live checklist with your Lumos contact.

Environments

EnvironmentBase URLMoney
SandboxSent with your sandbox keysTest only. Prices move like the market; no metal or money changes hands.
ProductionSent with your production keysReal metal and real money.

All paths in this guide start with /v1 and are added to the base URL.

Conventions

TopicRule
FormatJSON request and response bodies, UTF-8. Send Content-Type: application/json on any call with a body. Unknown request fields are ignored.
MoneyA string in AED with 2 decimals, for example "100.00". Never send money as a floating-point number.
MetalA string in grams with 6 decimals, for example "0.210062". Metal is Gold or Silver.
RoundingGrams are rounded down to the nearest 0.000001 g. When a customer asks for a quantity, the AED amount is rounded up to the nearest fils. Always show the customer the values Lumos returns.
TimesISO 8601 in UTC, for example 2026-10-19T11:47:30.513Z. Settlement days follow Dubai time (UTC+4).
DatesYYYY-MM-DD. Countries use two-letter ISO 3166-1 codes, for example AE.
CustomersYou address customers by your own identifier. It is unique within your account.
Listspage (from 1) and limit query parameters. Responses include a pagination object with page, limit, total and totalPages.
Request IDEvery response carries a requestId. Quote it when you contact Lumos about a call.

The legacy price fields price24K, sellPrice24K, silverPrice and sellPriceSilver are numbers in AED per gram, kept for display only. Quotes always use strings.

Signing requests

Every request carries three headers. Lumos rejects a request with a wrong signature, an unknown client, or a timestamp more than 300 seconds from Lumos's clock.

HeaderValue
X-Client-IdYour client ID
X-TimestampCurrent Unix time in seconds, for example 1792400000
X-SignatureLowercase hex HMAC-SHA256 of the string to sign, keyed with your client secret

The string to sign

Five lines joined by a single newline (\n), with no newline at the end:

  1. The HTTP method in upper case, for example POST.
  2. The path only, without the host or query, for example /v1/buy/preview.
  3. The query string with its key=value pairs sorted by key (then by value), joined by &, values exactly as sent. An empty line if there is no query.
  4. The SHA-256 hex digest of the exact body bytes you send. For a call with no body, the digest of the empty string.
  5. The same timestamp you send in X-Timestamp.

Hash the exact bytes you send. If your HTTP client re-serialises JSON after you sign it, the signature will not match. Serialise once, sign that string, and send that string.

Worked example

With the test secret sk_test_example_do_not_use and timestamp 1792400000, this request:

Request
POST /v1/buy/preview
{"customerIdentifier":"cust-1042","metal":"Gold","amount":"100.00"}

has this string to sign (the third line is empty because there is no query):

String to sign
POST
/v1/buy/preview

c7014d25957683fa0361479d4edf508328dbb9337686377f9aa0f00806fa9429
1792400000

and this signature:

X-Signature
f038e3e1458de3c0469f5e9709591715599ba5298462add50d7ae42149b2027a

Node.js

sign.js
import crypto from 'node:crypto';

export function signRequest(secret, method, path, query, body, timestamp) {
  const sortedQuery = query
    ? query.split('&').filter(Boolean)
        .map((p) => { const i = p.indexOf('='); return i < 0 ? [p, ''] : [p.slice(0, i), p.slice(i + 1)]; })
        .sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) : a[0] < b[0] ? -1 : 1))
        .map(([k, v]) => `${k}=${v}`).join('&')
    : '';
  const bodyHash = crypto.createHash('sha256').update(body ?? '', 'utf8').digest('hex');
  const toSign = [method.toUpperCase(), path, sortedQuery, bodyHash, timestamp].join('\n');
  return crypto.createHmac('sha256', secret).update(toSign, 'utf8').digest('hex');
}

// Usage
const body = JSON.stringify({ customerIdentifier: 'cust-1042', metal: 'Gold', amount: '100.00' });
const ts = String(Math.floor(Date.now() / 1000));
const res = await fetch(`${BASE_URL}/v1/buy/preview`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Client-Id': CLIENT_ID,
    'X-Timestamp': ts,
    'X-Signature': signRequest(CLIENT_SECRET, 'POST', '/v1/buy/preview', '', body, ts),
  },
  body,
});

Python

sign.py
import hashlib, hmac, json, time, requests

def sign_request(secret, method, path, query, body, timestamp):
    pairs = [p.partition("=")[::2] for p in query.split("&") if p]
    sorted_query = "&".join(f"{k}={v}" for k, v in sorted(pairs))
    body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
    to_sign = "\n".join([method.upper(), path, sorted_query, body_hash, timestamp])
    return hmac.new(secret.encode(), to_sign.encode(), hashlib.sha256).hexdigest()

body = json.dumps({"customerIdentifier": "cust-1042", "metal": "Gold", "amount": "100.00"}, separators=(",", ":"))
ts = str(int(time.time()))
res = requests.post(
    f"{BASE_URL}/v1/buy/preview",
    data=body.encode("utf-8"),
    headers={
        "Content-Type": "application/json",
        "X-Client-Id": CLIENT_ID,
        "X-Timestamp": ts,
        "X-Signature": sign_request(CLIENT_SECRET, "POST", "/v1/buy/preview", "", body, ts),
    },
)

Checking Lumos's responses

Every response from Lumos carries its own X-Timestamp and X-Signature. The signature is the HMAC-SHA256, with your client secret, of STATUS\nTIMESTAMP\nRAW_BODY, where STATUS is the HTTP status code, for example 201. Checking it proves the response came from Lumos and was not changed on the way.

Rotating your secret

Ask your Lumos contact for a new secret. During a rotation, both the old and the new secret are accepted, so you can switch your servers over without downtime. Then ask Lumos to retire the old one.

Retries and idempotency

Networks fail. To retry a POST safely, send an Idempotency-Key header with a unique value per action, for example a UUID, of at most 255 characters.

  • Same key and same body within 24 hours: you get the stored response of the first call. Nothing happens twice.
  • Same key with a different body: 409 idempotency_conflict.
  • Same key while the first call is still running: 409 idempotency_in_progress. Wait a moment and retry.

Independently of the key, a paymentReference can be used only once in your account (409 duplicate_payment_reference), and a quote can be placed only once (409 quote_used). These protect your customers from being charged twice.

If a call times out and you did not send a key, check the outcome before you retry: GET /v1/customers/{identifier}/transactions lists the customer's latest transactions.

Customers

Create a customer once, using your own ID as identifier. Calling create again with the same identifier returns the existing customer (200) instead of a new one (201). Use PATCH to change any detail later, including a renewed ID or a new KYC result.

KYC is your responsibility. You verify the customer and send Lumos the result.

POST/v1/customers
FieldDescription
identifierrequiredYour customer ID
firstName, lastNamerequiredAs on the ID document
dateOfBirthrequiredYYYY-MM-DD
nationality, countryrequiredISO country codes. country is the country of residence.
emailrequired
phonerequired{ "countryCode": "+971", "number": "501234567" }
addressrequiredUsed on invoices and for delivery. A single string or an object.
idTyperequiredEmiratesId, Passport or NationalId
kyc.kycIdrequiredYour KYC provider's reference
kyc.kycStatusrequiredApproved allows trading
kyc.issuingAuthority, kyc.issuedAt, kyc.expiresAtrequiredFrom the ID document. Dates as YYYY-MM-DD.
kyc.documentType, kyc.documentNumberrequiredThe document number is encrypted at rest
termsAccepted, termsVersionrequiredtrue and the version of the Lumos terms the customer accepted
genderoptional
Request body
{
  "identifier": "cust-1042",
  "firstName": "Mariam",
  "lastName": "Al Hashimi",
  "dateOfBirth": "1990-04-12",
  "nationality": "AE",
  "country": "AE",
  "email": "mariam@example.com",
  "phone": { "countryCode": "+971", "number": "501234567" },
  "address": "Apartment 1204, Marina Gate 1, Dubai Marina, Dubai",
  "idType": "EmiratesId",
  "kyc": {
    "kycId": "kyc_88231",
    "kycStatus": "Approved",
    "issuingAuthority": "ICP",
    "issuedAt": "2023-05-01",
    "expiresAt": "2028-04-30",
    "documentType": "EmiratesId",
    "documentNumber": "784-1990-1234567-1"
  },
  "termsAccepted": true,
  "termsVersion": "2026-10"
}

The response is the customer, with Lumos's customerId, a status of Active, Frozen or Closed, and createdAt and updatedAt.

When a customer can trade

A customer can get a quote only when all of these are true. Otherwise the quote call returns 400 and names what is missing.

  • Every required field is present.
  • kyc.kycStatus is Approved.
  • The ID has not expired (kyc.expiresAt is in the future).
  • termsAccepted is true.
  • The account status is Active.

Other customer calls

GET/v1/customers?page=1&limit=20

Your customers, one page at a time.

GET/v1/customers/{identifier}

One customer.

PATCH/v1/customers/{identifier}

Send only the fields that change. The same fields as create, except identifier.

GET/v1/customers/{identifier}/transactions

The customer's transactions, newest first. Filter with status, type (Buy, Sell), and from and to (date or date-time, inclusive).

Prices

GET/v1/prices/latest

Current buy and sell prices per gram, for display. To trade, always get a quote: the quote's price is the one the customer gets.

200 OK
{
  "id": "6ad60347abac68b6e7345dd6",
  "price24K": 476.05,
  "sellPrice24K": 461.97,
  "silverPrice": 5.67,
  "sellPriceSilver": 5.33,
  "currency": "AED",
  "createdAt": "2026-10-19T11:47:19.240Z",
  "updatedAt": "2026-10-19T11:47:19.240Z",
  "requestId": "req_hiK6XxBMFwQ8Eaf5"
}
GET/v1/prices/history?metal=Gold&range=30d

Daily average buy prices, oldest first, for charts. range is one of 7d, 30d, 6m, 1y, 3y, 5y, max.

When prices pause

If Lumos's price feed stops updating, Lumos pauses buying and selling for that metal rather than trade on an old price. Quote calls return 503 prices_paused, and your webhook receives prices.paused, then prices.resumed when trading restarts. Show the customer a short "prices are updating" message and let them retry.

Buying

A buy is a quote, then an order, then payment. The quote fixes the price for 3 minutes.

POST/v1/buy/preview
FieldDescription
customerIdentifierrequiredYour customer ID
metalrequiredGold or Silver
amountone ofAED value of metal to buy. The platform fee and VAT are added on top.
quantityone ofGrams to buy
platformFeeoptionalAED. Accepted only within the floor and ceiling agreed for your account; otherwise your default fee applies.

Send exactly one of amount or quantity.

200 OK
{
  "quoteId": "q_lv2jobxizuC-CowSZgrysg",
  "metal": "Gold",
  "currency": "AED",
  "price": "476.05",
  "quantity": "0.210062",
  "total": "100.00",
  "vat": "0.00",
  "platformFee": "0.00",
  "grandTotal": "100.00",
  "expiresAt": "2026-10-19T11:50:30.505Z",
  "requestId": "req_nT1vh8hYRkZOe7ar"
}

total is the metal value, grandTotal is what the customer pays. With a platform fee of AED 1.00 and VAT at 5% on the fee, the same buy has a grandTotal of AED 101.05.

POST/v1/buy
FieldDescription
quoteIdrequiredFrom the preview. Must not be expired or used.
statusoptionalCompleted if you have already taken payment; otherwise leave it out and the buy starts Pending.
paymentReferencewith CompletedYour payment ID. Unique in your account.
paymentMethodoptionalFree text for your records, for example wallet or card
201 Created
{
  "id": "txn_6ad60352abac68b6e7345dda",
  "type": "Buy",
  "metal": "Gold",
  "quantity": "0.210062",
  "currency": "AED",
  "quotedPrice": "476.05",
  "status": "Pending",
  "vat": "0.00",
  "platformFee": "0.00",
  "total": "100.00",
  "grandTotal": "100.00",
  "requestId": "req_g5U6zk8TomveGUPr"
}

Confirming payment

You take the payment, then tell Lumos. Either place the buy with status: "Completed" and your paymentReference in one call, or place it Pending and confirm with PATCH once payment clears. If payment fails, send Failed or Cancelled instead.

%%{init: {"theme":"base","themeVariables":{"darkMode":false,"background":"#FFFFFF","primaryColor":"#EEF5FC","primaryBorderColor":"#3B1EFF","primaryTextColor":"#2B2D3D","lineColor":"#2B2D3D","textColor":"#2B2D3D","actorBkg":"#EEF5FC","actorBorder":"#3B1EFF","actorTextColor":"#2B2D3D","actorLineColor":"#D7E8FA","signalColor":"#2B2D3D","signalTextColor":"#2B2D3D","labelBoxBkgColor":"#EEF5FC","labelBoxBorderColor":"#3B1EFF","noteBkgColor":"#5DF9C0","noteBorderColor":"#2B2D3D","activationBkgColor":"#D7E8FA","stateBkg":"#EEF5FC","stateBorder":"#3B1EFF","transitionColor":"#2B2D3D","transitionLabelColor":"#2B2D3D","edgeLabelBackground":"#FFFFFF","nodeBorder":"#3B1EFF","nodeTextColor":"#2B2D3D","stateLabelColor":"#2B2D3D","loopTextColor":"#2B2D3D","noteTextColor":"#2B2D3D","sequenceNumberColor":"#FFFFFF"}}}%%
sequenceDiagram
  participant App as Your app
  participant S as Your server
  participant L as Lumos
  App->>S: Buy AED 100 of gold
  S->>L: POST /v1/buy/preview
  L-->>S: quoteId, quantity, grandTotal
  S-->>App: Show quote (valid 3 minutes)
  App->>S: Confirm
  S->>S: Take payment
  S->>L: POST /v1/buy (status Completed, paymentReference)
  L-->>S: 201, status Completed
  L--)S: Webhook transaction.created
  
PATCH/v1/transactions/{transactionId}
Request body
{
  "status": "Completed",
  "paymentReference": "pay-77120931",
  "paymentMethod": "wallet"
}

status is Completed, Failed or Cancelled. paymentReference is required for Completed. An optional notes field is kept with the transaction.

Limits

Your account has a minimum order, and can have a maximum order and a daily volume limit if agreed with Lumos. An order that would cross one returns 400 limit_exceeded. GET /v1/partner shows your limits.

Selling

A sell is a quote, then an order. When the sell is placed, Lumos sets the grams aside so they cannot be sold twice. They come back to the customer if the sell fails, is cancelled or goes stale.

POST/v1/sell/preview

The same fields as a buy preview, without platformFee. amount is the AED value of metal to sell; quantity is grams. grandTotal is what the customer receives.

200 OK
{
  "quoteId": "q_NiV5zfV7Sja9hezaMx16LA",
  "metal": "Gold",
  "currency": "AED",
  "price": "461.97",
  "quantity": "0.050000",
  "total": "23.09",
  "vat": "0.00",
  "platformFee": "0.00",
  "grandTotal": "23.09",
  "expiresAt": "2026-10-19T11:50:30.529Z",
  "requestId": "req_hKzcxfJLz5tqYcIN"
}

Fees on sells

Sells carry no platform fee. A small-sell fee applies to sells below AED 100, with VAT on the fee, and is taken from the payout. For example, selling AED 50 of gold with a AED 2.00 fee and 5% VAT pays the customer AED 47.90. Nothing beyond the grams sold is ever taken from the customer's metal.

POST/v1/sell

Send the quoteId. The response has the same shape as a buy.

Paying your customer

You pay your customer. Place the sell Pending, pay the customer, then send PATCH /v1/transactions/{id} with Completed and your paymentReference. If you pay at once, place the sell with status: "Completed" and your paymentReference in one call. If the payout fails, send Failed or Cancelled and the grams go back to the customer.

Transaction statuses

StatusMeaning
PendingPlaced, waiting for payment or payout. A pending sell holds its grams.
CompletedFinal. The metal and money have moved. It never changes again; only a reversal can undo it.
FailedPayment or payout failed. Final.
CancelledCancelled before completion. Final.
StaleStill Pending after 10 minutes, so Lumos closed it. Final. Sell grams are released.
%%{init: {"theme":"base","themeVariables":{"darkMode":false,"background":"#FFFFFF","primaryColor":"#EEF5FC","primaryBorderColor":"#3B1EFF","primaryTextColor":"#2B2D3D","lineColor":"#2B2D3D","textColor":"#2B2D3D","actorBkg":"#EEF5FC","actorBorder":"#3B1EFF","actorTextColor":"#2B2D3D","actorLineColor":"#D7E8FA","signalColor":"#2B2D3D","signalTextColor":"#2B2D3D","labelBoxBkgColor":"#EEF5FC","labelBoxBorderColor":"#3B1EFF","noteBkgColor":"#5DF9C0","noteBorderColor":"#2B2D3D","activationBkgColor":"#D7E8FA","stateBkg":"#EEF5FC","stateBorder":"#3B1EFF","transitionColor":"#2B2D3D","transitionLabelColor":"#2B2D3D","edgeLabelBackground":"#FFFFFF","nodeBorder":"#3B1EFF","nodeTextColor":"#2B2D3D","stateLabelColor":"#2B2D3D","specialStateColor":"#3B1EFF","innerEndBackground":"#3B1EFF"}}}%%
stateDiagram-v2
  [*] --> Pending
  [*] --> Completed: placed with payment
  Pending --> Completed
  Pending --> Failed
  Pending --> Cancelled
  Pending --> Stale: after 10 minutes
  

Any other change returns 409 invalid_status_transition. Complete pending transactions within 10 minutes, and always act on the status Lumos returns.

GET/v1/transactions/{transactionId}

The full transaction, including paymentReference, the latest reversal if any, and a settlement block: Lumos's and your share (lumosAmount, partnerAmount), fees, VAT, and which settlement statement it is on.

Holdings

GET/v1/customers/{identifier}/holdings

Per metal, in grams and AED.

FieldMeaning
currentQuantityAll grams the customer owns, including grams set aside for a pending sell
availableQuantityGrams the customer can sell now
pendingQuantityGrams in pending sells
lockedQuantityGrams locked for another purpose. 0 today.
avgBuyPrice, investedAverage cost per gram and total cost of the grams held
currentValueGrams at today's buy price
sellValueGrams at today's sell price: what the customer would receive
returns, pnlPercentsellValue minus invested, in AED and percent
200 OK (gold)
{
  "currency": "AED",
  "gold": {
    "currentQuantity": "0.210062",
    "availableQuantity": "0.160062",
    "pendingQuantity": "0.050000",
    "lockedQuantity": "0.000000",
    "avgBuyPrice": "476.04",
    "invested": "100.00",
    "currentValue": "100.00",
    "sellValue": "97.04",
    "returns": "-2.96",
    "pnlPercent": -2.96
  },
  "silver": { … }
}

Straight after a buy, returns is slightly negative: it is the gap between the buy and sell price.

Webhooks

Lumos sends a POST to your webhook URL whenever something changes. Use webhooks to update your app at once, and the API to read the current state.

EventSent whenpayload
transaction.createdA buy or sell is placedThe transaction
transaction.status_changedA transaction changes status, or a reversal is doneThe transaction
customer.createdA customer is createdThe customer
customer.updatedA customer changesThe customer
settlement.readyA daily settlement statement is readyThe statement
prices.pausedTrading in a metal pauses{ "metal", "at" }
prices.resumedTrading in a metal resumes{ "metal", "at" }
Webhook body
{
  "eventType": "transaction.status_changed",
  "payload": {
    "id": "txn_6ad60352abac68b6e7345dda",
    "type": "Buy",
    "status": "Completed",
    "customerIdentifier": "cust-1042",
    "metal": "Gold",
    "quantity": "0.210062",
    "grandTotal": "100.00",
    …
  }
}

Headers and verification

HeaderValue
X-Event-IdUnique per event, the same on every retry. Use it to ignore duplicates.
X-TimestampUnix seconds when this attempt was sent
X-SignatureLowercase hex HMAC-SHA256, with your client secret, of TIMESTAMP\nRAW_BODY
Verify a webhook (Node.js)
import crypto from 'node:crypto';

export function isFromLumos(secret, timestamp, rawBody, signature) {
  const expected = crypto.createHmac('sha256', secret).update(`${timestamp}\n${rawBody}`, 'utf8').digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
  return fresh && signature.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

Delivery and retries

  • Reply with any 2xx status quickly, then do your work. Anything else counts as a failure.
  • Lumos retries after 10 seconds, 30 seconds, 1 minute, 5 minutes, 15 minutes, 30 minutes, then every hour, for up to 24 hours.
  • Events can arrive more than once and out of order. De-duplicate by X-Event-Id, and if order matters, read the transaction with GET /v1/transactions/{id}.
  • Missed events can be replayed. Ask your Lumos contact today; the partner portal will offer this as a button.

Settlement and invoices

Lumos makes a statement for you every day. Each statement covers one Dubai day and lists every transaction completed that day, with what each side owes, netted into one amount in one direction. The money itself moves on the schedule agreed for your account, for example every day, every two days or weekly, and one payment can cover several statements. It is paid by you, or by the company that settles for you.

GET/v1/settlements?from=2026-10-01&to=2026-10-31
A statement
{
  "settlementId": "stl_…",
  "date": "2026-10-19",
  "currency": "AED",
  "totalLumos": "…",
  "totalPartner": "…",
  "net": "…",
  "direction": "PartnerPaysLumos",
  "status": "Open"
}
FieldMeaning
totalLumosOwed to Lumos by you on this statement
totalPartnerOwed to you by Lumos on this statement
net, directionThe difference, and who pays it: PartnerPaysLumos or LumosPaysPartner
statusOpen until a payment covering it is confirmed, then Paid
GET/v1/settlements/{settlementId}

The statement with its lines: one per completed transaction, with owedToLumos and owedToPartner. A reversal appears as a negative line on a later statement. Your webhook receives settlement.ready when a statement is ready.

Invoices

GET/v1/transactions/{transactionId}/invoice

A link to the transaction's PDF invoice, valid for 10 minutes. The invoice is created when the transaction completes; before that the call returns 409 invoice_not_ready. After a reversal, the response also carries creditNoteUrl.

Refunds and reversals

A completed transaction never changes status. To undo one, for example after a duplicate payment, ask for a reversal. Lumos operations review it, and two people at Lumos must approve before it is done. Lumos puts the customer's metal position back and adjusts your next settlement statement; you settle any refund with your customer.

POST/v1/transactions/{transactionId}/reversal
Request body
{ "reason": "Customer paid twice by mistake" }

The response has a reversalId and a status: Requested, then Approved or Rejected, then Done. The transaction stays Completed; its reversal.status of Done shows it was undone, and you receive transaction.status_changed. A transaction can have one reversal request (409 reversal_exists).

All endpoints

MethodPathPurpose
GET/v1/partnerYour settings, limits and fees
POST/v1/customersCreate a customer
GET/v1/customersList customers
GET/v1/customers/{identifier}One customer
PATCH/v1/customers/{identifier}Update details or KYC
GET/v1/customers/{identifier}/holdingsHoldings
GET/v1/customers/{identifier}/transactionsTransaction history
GET/v1/prices/latestCurrent prices
GET/v1/prices/historyDaily price history
POST/v1/buy/previewBuy quote
POST/v1/buyPlace a buy
POST/v1/sell/previewSell quote
POST/v1/sellPlace a sell
GET/v1/transactions/{transactionId}One transaction
PATCH/v1/transactions/{transactionId}Complete, fail or cancel a pending transaction
GET/v1/transactions/{transactionId}/invoiceInvoice PDF link
POST/v1/transactions/{transactionId}/reversalAsk for a reversal
GET/v1/settlementsSettlement statements
GET/v1/settlements/{settlementId}One statement with lines

A full machine-readable reference (OpenAPI 3) is available alongside your sandbox at /docs/reference.

Errors

Errors use standard HTTP status codes and the same body. Branch on code; show or log message.

400 Bad Request
{
  "code": "insufficient_balance",
  "message": "Not enough gold: 0.160062 g available",
  "requestId": "req_JKb6unQPMTH0YiKR"
}
StatusCodeWhat to do
400validation_errorA field is missing or malformed. The message names it.
400kyc_requiredKYC is not Approved or the ID has expired. Send updated KYC with PATCH.
400insufficient_balanceThe customer does not have enough available grams.
400limit_exceededBelow the minimum order, or above the maximum order or daily volume limit.
401invalid_clientMissing or unknown X-Client-Id.
401invalid_signatureSignature missing or wrong. Check the string to sign and the body bytes.
401invalid_timestampTimestamp missing or more than 300 seconds off. Check your server clock.
403customer_not_activeThe customer is Frozen or Closed.
403feature_disabled, partner_suspendedBuying or selling is switched off for your account. Contact Lumos.
404not_foundThe customer, quote, transaction or statement does not exist in your account.
409quote_usedThe quote was already placed. Get a new quote.
409duplicate_payment_referenceThis paymentReference was used before.
409invalid_status_transitionThe transaction is no longer Pending. Read it to see its status.
409idempotency_conflict, idempotency_in_progressSee Retries and idempotency.
409reversal_exists, invoice_not_readySee Reversals and Invoices.
410quote_expiredMore than 3 minutes passed. Get a new quote and show it to the customer.
429rate_limitedToo many requests. Back off and retry.
503prices_pausedTrading in this metal is paused. Retry after prices.resumed.
500internal_errorRetry with the same Idempotency-Key. If it persists, contact Lumos with the requestId.

Postman collection

Postman lets your engineers try every call without writing code. The collection signs each request automatically and passes ids from one call to the next, so you can run a full customer, buy and sell sequence in a few minutes.

  1. In Postman, choose Import, then Raw text, and paste the collection. Or save the file and import it.
  2. Open the collection's Variables tab and set baseUrl, clientId and clientSecret to your sandbox values.
  3. Run the folders from top to bottom: Partner, Customers, Prices, Buy and sell, Transactions, Settlement.

Keep your client secret out of shared Postman workspaces. Use a personal environment or the variable's current value, which Postman does not sync.

Go-live checklist

Work through these in the sandbox, then share the results with your Lumos contact.

  • Requests are signed on your server. No Lumos secret is in your app, your website or your logs.
  • Your server clock is synced (NTP), so timestamps stay within 300 seconds.
  • You verify Lumos's signature on webhooks, de-duplicate by X-Event-Id, and reply 2xx quickly.
  • Customers are created with every required field, and KYC updates and ID renewals are sent with PATCH.
  • Your app shows the quote's quantity, price and grandTotal, and asks for a new quote after 3 minutes.
  • Every pending transaction is completed, failed or cancelled within 10 minutes.
  • You send an Idempotency-Key on every POST and a unique paymentReference per payment.
  • You handle every error code above, including prices_paused and quote_expired.
  • Your finance team can read the daily settlement statements and match them to your own records.
  • You have named a technical contact and an operations contact for incidents.