Build on Codeproof
Quote, order with an Idempotency-Key, reveal each sealed code exactly once — then follow every outcome as a signed, sequenced event.
Getting started
The Codeproof API sells prepaid brand codes from your prepaid B2B wallet. You get a quote, place an order with anIdempotency-Key, and reveal each sealed code exactly once. Every outcome is also a sequenced, signed event.
- JSON in and out. Money is a string of integer paise in fields ending
Minor—"49000"is ₹490.00. Never parse it as a float. - Timestamps are ISO 8601. Errors are
{ "error": { "code", "message", "details"? } }— branch oncode. - Plaintext codes appear in exactly one response: the reveal. Everything else carries
****-****-1234and acodeId. - Codeproof is in build. The sandbox runs the real order engine, vault and ledger against a simulated supplier.
https://codeproof.fluxusforge.in # every request Authorization: Bearer cp_…
Your first order in 4 calls
- Catalogue.
GET /v1/catalog— pick a SKU whereavailableis true;unitPriceMinoris your price. - Quote.
POST /v1/quoteswithskuandqty. The price holds untilexpiresAt. - Order.
POST /v1/orderswith thequoteIdand a freshIdempotency-Key.201is final;202means held while we confirm with the supplier. - Reveal.
POST …/codes/{codeId}/revealwithX-Device-Id. You get the plaintext once — hand it to your customer, never log it.
# 1 · catalogue curl -s https://codeproof.fluxusforge.in/v1/catalog -H "Authorization: Bearer $KEY" # 2 · quote (price held 60 s) curl -s https://codeproof.fluxusforge.in/v1/quotes -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" -d '{"sku":"GPLAY-IN-500","qty":1}' # 3 · order — keep the key; reuse it on retry curl -s https://codeproof.fluxusforge.in/v1/orders -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d '{"quoteId":"<quoteId>","clientReference":"txn-8841"}' # 4 · reveal once curl -s -X POST https://codeproof.fluxusforge.in/v1/orders/<orderId>/codes/<codeId>/reveal \ -H "Authorization: Bearer $KEY" -H "X-Device-Id: pos-12"
Authentication
Send an API key as a Bearer token on every /v1 request. Keys are created by partner admins in the portal under Developers, shown once, and stored by us only as a hash.
- Keys start with
cp_. They are issued per deployment: a sandbox key works only against the sandbox, a production key only against production. - Keep keys on your servers. Never ship one in a mobile app or a browser.
- A revoked or unknown key returns
401 UNAUTHORIZEDimmediately.
Authorization: Bearer cp_… # keys start with cp_ and are issued per # deployment: a sandbox key only works in # the sandbox, a production key only in production
Sandbox
The sandbox uses a simulated supplier that misbehaves on purpose, so you can build against every outcome before go-live. Codes are fake and no money moves. Fluxus Forge credits test balance to your sandbox wallet on request.
Sandbox SKUs
| SKU | Face | Behaviour | What happens |
|---|---|---|---|
GPLAY-IN-100 | ₹100 | SIM-OK | Fulfils in full. |
GPLAY-IN-500 | ₹500 | SIM-OK | Fulfils in full. |
GPLAY-IN-1000 | ₹1,000 | SIM-OK | Fulfils in full. |
PSN-IN-1000 | ₹1,000 | SIM-OK | Fulfils in full. |
PSN-IN-2000 | ₹2,000 | SIM-PARTIAL | Delivers 73% of the quantity (always at least one short) → PARTIALLY_FULFILLED; the rest is released. |
XBOX-IN-1000 | ₹1,000 | SIM-TIMEOUT | Supplier reports unknown → 202 SUPPLIER_AMBIGUOUS; the resolver later finds the codes → FULFILLED. |
STEAM-IN-800 | ₹800 | SIM-OOS | Out of stock → FAILED (OUT_OF_STOCK), full release, no charge. |
NETFLIX-IN-500 | ₹500 | SIM-WRONGREGION | Supplier sends US-region codes → quarantined (REGION_MISMATCH), you are not charged for them. |
Catalogue
GET/v1/catalog
List active SKUs with your live price, or the reason a SKU cannot be quoted right now.
| Parameter | Type | Description |
|---|---|---|
region | string | Filter by region, e.g. IN. |
brand | string | Filter by exact brand name, e.g. Google Play. |
- unavailableReason is one of FX_UNAVAILABLE, FX_STALE, MARGIN_BELOW_FLOOR, EXPIRY_TOO_SHORT, RESALE_NOT_ALLOWED, INACTIVE.
curl -s "https://codeproof.fluxusforge.in/v1/catalog?region=IN" \ -H "Authorization: Bearer $CODEPROOF_KEY"
{ "items": [ { "sku": "GPLAY-IN-500", "brand": "Google Play", "product": "Google Play gift code", "region": "IN", "currency": "INR", "faceValueMinor": "50000", "available": true, "unitPriceMinor": "49000", "terms": null }, { "sku": "STEAM-IN-800", "brand": "Steam", "available": false, "unavailableReason": "FX_STALE", "...": "…" } ] }
Quotes
POST/v1/quotes
Fix a price for a SKU and quantity until expiresAt (60 seconds by default).
| Parameter | Type | Description |
|---|---|---|
sku | string | A SKU from the catalogue. |
qty | integer | 1 to 100 codes. |
curl -s https://codeproof.fluxusforge.in/v1/quotes \ -H "Authorization: Bearer $CODEPROOF_KEY" \ -H "Content-Type: application/json" \ -d '{"sku":"GPLAY-IN-500","qty":2}'
{ "quoteId": "cmg3q8t2k0001", "sku": "GPLAY-IN-500", "qty": 2, "unitPriceMinor": "49000", "totalMinor": "98000", "currency": "INR", "expiresAt": "2026-09-29T10:15:00.000Z" }
Orders
POST/v1/orders
Buy the quoted codes. Reserves money, buys once from the supplier, and returns the order with masked codes.
| Parameter | Type | Description |
|---|---|---|
Idempotency-Key | string (8–128) | Unique per checkout attempt. Retrying with the same key and body returns the original result. |
quoteId | string | From POST /v1/quotes. One order per quote. |
clientReference | string (≤128) | Your own reference; echoed in the order and every event. |
deliveryMode | API | MERCHANT_APP | CONSUMER_APP | Defaults to API. |
- PRICE_STALE carries details.freshQuote — show the new price, then confirm with a NEW Idempotency-Key.
- INSUFFICIENT_FUNDS carries details.availableMinor and details.requiredMinor.
- chargedMinor = unitPriceMinor × deliveredQty. Undelivered units are released in the same transaction.
curl -s https://codeproof.fluxusforge.in/v1/orders \ -H "Authorization: Bearer $CODEPROOF_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1c2a52-7d8e-4c1b-9a51-3f0e0c2b9d11" \ -d '{"quoteId":"cmg3q8t2k0001","clientReference":"txn-8841"}'
{ "orderId": "cmg3qa1xk0004", "state": "FULFILLED", "sku": "GPLAY-IN-500", "qty": 2, "deliveredQty": 2, "quarantinedQty": 0, "unitPriceMinor": "49000", "chargedMinor": "98000", "currency": "INR", "deliveryMode": "API", "clientReference": "txn-8841", "failureCode": null, "createdAt": "2026-09-29T10:14:12.000Z", "codes": [ { "codeId": "cmg3qa1xm0005", "masked": "****-****-7Q2K", "status": "DELIVERED", "expiresAt": "2027-09-29T00:00:00.000Z", "revealedAt": null } ] }
GET/v1/orders/{id}
Current state of one of your orders, with masked codes only. Poll this for ambiguous orders.
| Parameter | Type | Description |
|---|---|---|
id | string | orderId. |
curl -s https://codeproof.fluxusforge.in/v1/orders/cmg3qa1xk0004 \ -H "Authorization: Bearer $CODEPROOF_KEY"
{ "orderId": "cmg3qa1xk0004", "state": "SUPPLIER_AMBIGUOUS", "deliveredQty": 0, "codes": [], "...": "…" }
Order states
| State | Meaning | Your money |
|---|---|---|
CREATED | Accepted; status, tier caps and balance checked. | Nothing moved yet. |
WALLET_RESERVED | The total is held under a lock on your wallet. | available → reserved. |
SUPPLIER_PENDING | Bought once, with the order id as the supplier reference. | Still reserved. |
SUPPLIER_AMBIGUOUS | Supplier did not answer definitively. A resolver checks the status of our reference. | Held — never refunded blind, never re-bought. |
FULFILLED | Every code delivered and sealed. | reserved → supplier float + margin. |
PARTIALLY_FULFILLED | Some codes delivered (fewer arrived, or some were quarantined). | Charged for delivered units; the rest released to available. |
FAILED | No codes delivered. See failureCode. | Fully released to available. |
Handling ambiguous orders
If the supplier times out or answers unclearly after we have sent the purchase, the order returns 202 with state SUPPLIER_AMBIGUOUS. The money stays reserved while a resolver asks the supplier about our reference.
- Never retry with a new Idempotency-Key. A new key is a new purchase. Retrying with the same key and body is always safe — you get the current state back.
- Wait for
order.fulfilled,order.partialororder.failedon your webhook, or pollGET /v1/orders/{id}. - If the supplier never received the order, it fails and the money is released after a grace period. You are never charged for codes you did not get.
// 202 → SUPPLIER_AMBIGUOUS: money is held, not lost. // Do NOT retry with a new Idempotency-Key. async function waitForFinal(orderId) { for (let i = 0; i < 40; i++) { const o = await (await fetch(`https://codeproof.fluxusforge.in/v1/orders/${orderId}`, { headers: { Authorization: `Bearer ${KEY}` }, })).json(); if (!['CREATED', 'WALLET_RESERVED', 'SUPPLIER_PENDING', 'SUPPLIER_AMBIGUOUS'].includes(o.state)) return o; await new Promise((r) => setTimeout(r, 15000)); } // still ambiguous: keep waiting for the webhook }
Reveal
POST/v1/orders/{orderId}/codes/{codeId}/reveal
Return the plaintext code — exactly once. The flip, the decrypt and the audit row happen in one transaction.
| Parameter | Type | Description |
|---|---|---|
orderId | string | The order that owns the code. |
codeId | string | From order.codes[].codeId. |
X-Device-Id | string | A stable id of the device or terminal revealing the code. Recorded in the audit trail. |
- A second call returns 409 ALREADY_REVEALED with details.revealedAt. Every attempt is audited.
- Reveals are limited to 30 per minute per partner.
curl -s -X POST \ https://codeproof.fluxusforge.in/v1/orders/cmg3qa1xk0004/codes/cmg3qa1xm0005/reveal \ -H "Authorization: Bearer $CODEPROOF_KEY" \ -H "X-Device-Id: pos-terminal-12"
{ "codeId": "cmg3qa1xm0005", "code": "ABCD-EFGH-7Q2K", "revealedAt": "2026-09-29T10:16:40.000Z" }
Wallet
curl -s https://codeproof.fluxusforge.in/v1/wallet -H "Authorization: Bearer $CODEPROOF_KEY"
{ "currency": "INR", "availableMinor": "4852500", "reservedMinor": "49000" }
GET/v1/wallet/ledger
Your ledger postings, newest first. Positive amounts are money in.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Default 100, max 500. |
curl -s "https://codeproof.fluxusforge.in/v1/wallet/ledger?limit=50" -H "Authorization: Bearer $CODEPROOF_KEY"
{ "entries": [ { "at": "…", "bucket": "reserved", "kind": "fulfil", "reference": "cmg3qa1xk0004", "memo": null, "amountMinor": "-98000" }, { "at": "…", "bucket": "available", "kind": "reserve", "reference": "cmg3qa1xk0004", "memo": null, "amountMinor": "-98000" }, { "at": "…", "bucket": "available", "kind": "topup", "reference": "UTR2026092900421", "memo": null, "amountMinor": "5000000" } ] }
Top-ups arrive by bank transfer to your virtual account and are credited against the bank UTR — the same UTR is never credited twice. Each credit emits wallet.credited.
Events & webhooks
GET/v1/events
Replay your event stream from any sequence number — the same events your webhook receives.
| Parameter | Type | Description |
|---|---|---|
since | integer string | Return events with seq greater than this. Default 0. |
limit | integer | Default 100, max 500. Oldest first. |
curl -s "https://codeproof.fluxusforge.in/v1/events?since=1040" -H "Authorization: Bearer $CODEPROOF_KEY"
{ "events": [ { "seq": "1041", "type": "order.fulfilled", "createdAt": "…", "data": { "orderId": "…", "clientReference": "txn-8841", "codes": [ … ] } }, { "seq": "1042", "type": "wallet.credited", "createdAt": "…", "data": { "amountMinor": "5000000", "utr": "UTR2026092900421" } } ] }
Webhook delivery
- Set an https URL in the portal; you get a signing secret once. Admins can send a
pingevent from the portal to test it. - Each event is POSTed as JSON with
X-Codeproof-Event,X-Codeproof-SeqandX-Codeproof-Signature. - Delivered in
seqorder per partner. A failing delivery holds later events back, so you never see them out of order. - Respond 2xx within 5 seconds. Failures retry with exponential backoff (30 s doubling, capped at 1 hour). Events are never dropped.
- Deliveries are at-least-once: de-duplicate on
seq.
POST https://your-app.example/codeproof/webhook Content-Type: application/json X-Codeproof-Event: order.partial X-Codeproof-Seq: 1042 X-Codeproof-Signature: t=1790000000,v1=5f2c…e9 {"seq":"1042","type":"order.partial","createdAt":"…","data":{…}}
Event types
order.fulfilled { "orderId": "…", "clientReference": "txn-8841", "codes": [{ "codeId": "…", "masked": "****-****-7Q2K", "expiresAt": "…" }] }
order.partial { "orderId": "…", "clientReference": "txn-8841", "requested": 10, "delivered": 7, "refundedMinor": "147000", "codes": [ … ] }
order.failed { "orderId": "…", "clientReference": "txn-8841", "failureCode": "OUT_OF_STOCK" }
order.ambiguous { "orderId": "…", "clientReference": "txn-8841" }
code.quarantined { "orderId": "…", "codeId": "…", "reason": "REGION_MISMATCH" }
wallet.credited { "amountMinor": "5000000", "utr": "UTR2026092900421" }
dispute.updated { "disputeId": "…", "orderId": "…", "codeId": "…", "status": "CREDITED", "creditedMinor": "49000", "resolution": "Supplier confirmed invalid code." }
ping { "message": "Codeproof test event", "requestedBy": "you@company.com" }
Verifying webhooks
- Read
X-Codeproof-Signature: t=<unix seconds>,v1=<hex>. - Compute HMAC-SHA256 with your secret over
`${t}.${rawBody}`— the raw bytes, not re-serialised JSON. - Compare with
v1in constant time. - Reject if
tis more than 5 minutes from your clock, to stop replays.
import { createHmac, timingSafeEqual } from 'node:crypto'; import express from 'express'; const SECRET = process.env.CODEPROOF_WEBHOOK_SECRET; // shown once in the portal const TOLERANCE_S = 300; // 5 minutes const app = express(); // Verify the RAW body — re-serialised JSON will not match. app.post('/codeproof/webhook', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('X-Codeproof-Signature') ?? ''; const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2))); const t = Number(parts.t); const body = req.body.toString('utf8'); const expected = createHmac('sha256', SECRET).update(`${t}.${body}`).digest(); const given = Buffer.from(parts.v1 ?? '', 'hex'); const fresh = Number.isFinite(t) && Math.abs(Date.now() / 1000 - t) <= TOLERANCE_S; if (!fresh || given.length !== expected.length || !timingSafeEqual(given, expected)) { return res.sendStatus(400); } const event = JSON.parse(body); // { seq, type, createdAt, data } // Idempotent: skip if you have already processed event.seq. res.sendStatus(200); // any 2xx within 5 s acknowledges });
Disputes
Portal only for now — /v1 has no dispute endpoints yet. Raise a dispute in the partner portal from the order page (“Report a problem” on a code) or under Disputes. A dispute is always about one code.
- One open dispute per code; only codes from delivered or partly delivered orders can be disputed.
- Reasons:
ALREADY_REDEEMED,INVALID,NOT_RECEIVED,WRONG_DENOMINATION,OTHER. - Statuses:
OPEN→UNDER_REVIEW→CREDITEDorREJECTED. A credit returns one unit’s price to your available balance; an unrevealed code becomes void. - Every change emits
dispute.updatedto your webhook and event stream.
Errors
Every error has the same shape. Branch on code; show message. The table lists every code the backend can return, and where.
| Code | HTTP | Where | Meaning |
|---|---|---|---|
UNAUTHORIZED | 401 | API · portal · console | Missing or invalid Bearer key, or no session. |
FORBIDDEN | 403 | portal · console | Your role may not do this. |
CSRF | 403 | portal · console | Browser request without X-Requested-With: codeproof. |
PARTNER_INACTIVE | 403 | API | Partner is pending, suspended, frozen or closed. |
LIMIT_EXCEEDED | 403 | API | Daily or monthly tier cap would be exceeded. |
INSUFFICIENT_FUNDS | 402 | API | Available balance too low. details.availableMinor, details.requiredMinor. |
INVALID_REQUEST | 400 | all | Body or query failed validation; details has the field errors. |
BAD_REQUEST | 400 | all | Malformed request (e.g. invalid JSON). |
INVALID_QTY | 400 | API | qty must be 1–100. |
IDEMPOTENCY_KEY_REQUIRED | 400 | API | POST /v1/orders needs an Idempotency-Key of 8–128 characters. |
DEVICE_ID_REQUIRED | 400 | API | Reveal needs X-Device-Id. |
INVALID_AMOUNT | 400 | console | Credit or float amount must be positive. |
INVALID_EMAIL | 400 | auth | Email is not valid. |
INVALID_ROLE | 400 | portal · console | Role does not match the account kind. |
PARTNER_REQUIRED | 400 | console | Partner users need a partner. |
STAFF_DOMAIN | 400 | console | Staff must use an @fluxusforge.in address. |
CANNOT_DISABLE_SELF | 400 | portal · console | You cannot disable your own user. |
TOKEN_INVALID | 400 | auth | Sign-in link is not valid. |
TOKEN_EXPIRED | 400 | auth | Sign-in link expired. |
TOKEN_USED | 400 | auth | Sign-in link already used. |
WRONG_BROWSER | 400 | auth | Link opened in a different browser than the one that requested it. |
INVALID_RANGE | 400 | portal · console | Statement/export: from must be before to (YYYY-MM-DD). |
RANGE_TOO_LONG | 400 | portal · console | Statement/export range longer than 366 days. |
NOT_FOUND | 404 | all | Unknown order, code, SKU, quote or route. Other partners’ data is never visible. |
PRICE_STALE | 409 | API | Quote expired. details.freshQuote is a new quote. |
QUOTE_USED | 409 | API | Quote already used by another order. |
IDEMPOTENCY_CONFLICT | 409 | API | Idempotency-Key reused with a different body. |
ALREADY_REVEALED | 409 | API · portal | Code revealed before. details.revealedAt. |
NOT_REVEALABLE | 409 | API · portal | Code is not revealable (e.g. quarantined or void). |
DISPUTE_EXISTS | 409 | portal | The code already has an open dispute. |
NOT_DISPUTABLE | 409 | portal | Order not delivered, or code quarantined/void. |
DISPUTE_CLOSED | 409 | console | Dispute already credited or rejected. |
USER_EXISTS | 409 | portal · console | A user with this email already exists. |
WEBHOOK_NOT_CONFIGURED | 409 | portal | Set a webhook URL before sending a test ping. |
SKU_UNAVAILABLE | 422 | API | SKU cannot be quoted now; message has the reason. |
RATE_LIMITED | 429 | API · portal | Reveal rate limit (30/min per partner). |
INTERNAL | 500 | all | Our fault. Safe to retry an order with the same Idempotency-Key. |
Idempotency
POST /v1/ordersrequiresIdempotency-Key(8–128 characters). A UUID per checkout attempt is ideal.- Same key + same body → the original result, no second purchase. Same key + different body →
409 IDEMPOTENCY_CONFLICT. - A quote can back one order only: a new key with a used quote returns
409 QUOTE_USED. - After
PRICE_STALE, the fresh quote is a new checkout attempt — use a new key.
// Generate once per checkout attempt and store it with the attempt. const key = checkout.idempotencyKey ??= crypto.randomUUID(); for (let attempt = 0; attempt < 3; attempt++) { try { const res = await createOrder(quoteId, key); // same key, same body return res; // original result on replay } catch (e) { if (!isNetworkError(e)) throw e; // only retry transport errors } }
Rate limits
| Scope | Limit | When exceeded |
|---|---|---|
/v1/*, /api/* | 20 requests/s per IP, burst 40 (at the edge) | HTTP 429 from the edge |
| Reveal | 30 reveals/min per partner | 429 RATE_LIMITED |
| Sign-in links | 10/min per IP, burst 5 | HTTP 429 from the edge |
Back off exponentially on 429. Edge 429s may not carry the JSON error body.
Changelog
- Events
dispute.updatedandping. Disputes in the partner portal. Webhook delivery log, retry and test ping in the portal. Wallet statements as CSV. -
/v1: catalogue, quotes, idempotent orders, single reveal, wallet and ledger, sequenced events and signed webhooks.