Codeproof · Fluxus ForgeVault · Ledger · Reveal
Sealing000%
Developers · API v1

Build on Codeproof

Quote, order with an Idempotency-Key, reveal each sealed code exactly once — then follow every outcome as a signed, sequenced event.

Partner API · v1

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 on code.
  • Plaintext codes appear in exactly one response: the reveal. Everything else carries ****-****-1234 and a codeId.
  • Codeproof is in build. The sandbox runs the real order engine, vault and ledger against a simulated supplier.
Base URL
https://codeproof.fluxusforge.in

# every request
Authorization: Bearer cp_…

Your first order in 4 calls

  1. Catalogue. GET /v1/catalog — pick a SKU where available is true; unitPriceMinor is your price.
  2. Quote. POST /v1/quotes with sku and qty. The price holds until expiresAt.
  3. Order. POST /v1/orders with the quoteId and a fresh Idempotency-Key. 201 is final; 202 means held while we confirm with the supplier.
  4. Reveal. POST …/codes/{codeId}/reveal with X-Device-Id. You get the plaintext once — hand it to your customer, never log it.
The whole flow · curl
# 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 UNAUTHORIZED immediately.
Header
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

SKUFaceBehaviourWhat happens
GPLAY-IN-100₹100SIM-OKFulfils in full.
GPLAY-IN-500₹500SIM-OKFulfils in full.
GPLAY-IN-1000₹1,000SIM-OKFulfils in full.
PSN-IN-1000₹1,000SIM-OKFulfils in full.
PSN-IN-2000₹2,000SIM-PARTIALDelivers 73% of the quantity (always at least one short) → PARTIALLY_FULFILLED; the rest is released.
XBOX-IN-1000₹1,000SIM-TIMEOUTSupplier reports unknown → 202 SUPPLIER_AMBIGUOUS; the resolver later finds the codes → FULFILLED.
STEAM-IN-800₹800SIM-OOSOut of stock → FAILED (OUT_OF_STOCK), full release, no charge.
NETFLIX-IN-500₹500SIM-WRONGREGIONSupplier 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.

Parameters
ParameterTypeDescription
region
query · optional
stringFilter by region, e.g. IN.
brand
query · optional
stringFilter 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"
Response · 200
{
  "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).

Parameters
ParameterTypeDescription
sku
body · required
stringA SKU from the catalogue.
qty
body · required
integer1 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}'
Response · 201
{
  "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.

Parameters
ParameterTypeDescription
Idempotency-Key
header · required
string (8–128)Unique per checkout attempt. Retrying with the same key and body returns the original result.
quoteId
body · required
stringFrom POST /v1/quotes. One order per quote.
clientReference
body · optional
string (≤128)Your own reference; echoed in the order and every event.
deliveryMode
body · optional
API | MERCHANT_APP | CONSUMER_APPDefaults 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"}'
Response · 201 · 202
{
  "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.

Parameters
ParameterTypeDescription
id
path · required
stringorderId.
curl -s https://codeproof.fluxusforge.in/v1/orders/cmg3qa1xk0004 \
  -H "Authorization: Bearer $CODEPROOF_KEY"
Response · 200
{ "orderId": "cmg3qa1xk0004", "state": "SUPPLIER_AMBIGUOUS", "deliveredQty": 0, "codes": [], "...": "…" }

Order states

StateMeaningYour money
CREATEDAccepted; status, tier caps and balance checked.Nothing moved yet.
WALLET_RESERVEDThe total is held under a lock on your wallet.available → reserved.
SUPPLIER_PENDINGBought once, with the order id as the supplier reference.Still reserved.
SUPPLIER_AMBIGUOUSSupplier did not answer definitively. A resolver checks the status of our reference.Held — never refunded blind, never re-bought.
FULFILLEDEvery code delivered and sealed.reserved → supplier float + margin.
PARTIALLY_FULFILLEDSome codes delivered (fewer arrived, or some were quarantined).Charged for delivered units; the rest released to available.
FAILEDNo 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.partial or order.failed on your webhook, or poll GET /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.
Wait for the outcome · Node
// 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.

Parameters
ParameterTypeDescription
orderId
path · required
stringThe order that owns the code.
codeId
path · required
stringFrom order.codes[].codeId.
X-Device-Id
header · required
stringA 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"
Response · 200
{
  "codeId": "cmg3qa1xm0005",
  "code": "ABCD-EFGH-7Q2K",
  "revealedAt": "2026-09-29T10:16:40.000Z"
}

Wallet

GET/v1/wallet

Your available and reserved balances — sums of ledger postings, never a stored number.

curl -s https://codeproof.fluxusforge.in/v1/wallet -H "Authorization: Bearer $CODEPROOF_KEY"
Response · 200
{ "currency": "INR", "availableMinor": "4852500", "reservedMinor": "49000" }

GET/v1/wallet/ledger

Your ledger postings, newest first. Positive amounts are money in.

Parameters
ParameterTypeDescription
limit
query · optional
integerDefault 100, max 500.
curl -s "https://codeproof.fluxusforge.in/v1/wallet/ledger?limit=50" -H "Authorization: Bearer $CODEPROOF_KEY"
Response · 200
{
  "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.

Parameters
ParameterTypeDescription
since
query · optional
integer stringReturn events with seq greater than this. Default 0.
limit
query · optional
integerDefault 100, max 500. Oldest first.
curl -s "https://codeproof.fluxusforge.in/v1/events?since=1040" -H "Authorization: Bearer $CODEPROOF_KEY"
Response · 200
{
  "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 ping event from the portal to test it.
  • Each event is POSTed as JSON with X-Codeproof-Event, X-Codeproof-Seq and X-Codeproof-Signature.
  • Delivered in seq order 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.
Delivery request
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 All codes delivered.
{ "orderId": "…", "clientReference": "txn-8841",
  "codes": [{ "codeId": "…", "masked": "****-****-7Q2K", "expiresAt": "…" }] }
order.partial Some codes delivered; the rest released.
{ "orderId": "…", "clientReference": "txn-8841",
  "requested": 10, "delivered": 7, "refundedMinor": "147000", "codes": [ … ] }
order.failed No codes; full release.
{ "orderId": "…", "clientReference": "txn-8841", "failureCode": "OUT_OF_STOCK" }
order.ambiguous Money held while we confirm with the supplier.
{ "orderId": "…", "clientReference": "txn-8841" }
code.quarantined A code failed validation and was not delivered.
{ "orderId": "…", "codeId": "…", "reason": "REGION_MISMATCH" }
wallet.credited A bank top-up landed (idempotent on UTR).
{ "amountMinor": "5000000", "utr": "UTR2026092900421" }
dispute.updated A dispute was raised, reviewed or resolved.
{ "disputeId": "…", "orderId": "…", "codeId": "…", "status": "CREDITED",
  "creditedMinor": "49000", "resolution": "Supplier confirmed invalid code." }
ping Sent by "Send test ping" in the portal.
{ "message": "Codeproof test event", "requestedBy": "you@company.com" }

Verifying webhooks

  1. Read X-Codeproof-Signature: t=<unix seconds>,v1=<hex>.
  2. Compute HMAC-SHA256 with your secret over `${t}.${rawBody}` — the raw bytes, not re-serialised JSON.
  3. Compare with v1 in constant time.
  4. Reject if t is 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 → CREDITED or REJECTED. A credit returns one unit’s price to your available balance; an unrevealed code becomes void.
  • Every change emits dispute.updated to 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.

CodeHTTPWhereMeaning
UNAUTHORIZED401API · portal · consoleMissing or invalid Bearer key, or no session.
FORBIDDEN403portal · consoleYour role may not do this.
CSRF403portal · consoleBrowser request without X-Requested-With: codeproof.
PARTNER_INACTIVE403APIPartner is pending, suspended, frozen or closed.
LIMIT_EXCEEDED403APIDaily or monthly tier cap would be exceeded.
INSUFFICIENT_FUNDS402APIAvailable balance too low. details.availableMinor, details.requiredMinor.
INVALID_REQUEST400allBody or query failed validation; details has the field errors.
BAD_REQUEST400allMalformed request (e.g. invalid JSON).
INVALID_QTY400APIqty must be 1–100.
IDEMPOTENCY_KEY_REQUIRED400APIPOST /v1/orders needs an Idempotency-Key of 8–128 characters.
DEVICE_ID_REQUIRED400APIReveal needs X-Device-Id.
INVALID_AMOUNT400consoleCredit or float amount must be positive.
INVALID_EMAIL400authEmail is not valid.
INVALID_ROLE400portal · consoleRole does not match the account kind.
PARTNER_REQUIRED400consolePartner users need a partner.
STAFF_DOMAIN400consoleStaff must use an @fluxusforge.in address.
CANNOT_DISABLE_SELF400portal · consoleYou cannot disable your own user.
TOKEN_INVALID400authSign-in link is not valid.
TOKEN_EXPIRED400authSign-in link expired.
TOKEN_USED400authSign-in link already used.
WRONG_BROWSER400authLink opened in a different browser than the one that requested it.
INVALID_RANGE400portal · consoleStatement/export: from must be before to (YYYY-MM-DD).
RANGE_TOO_LONG400portal · consoleStatement/export range longer than 366 days.
NOT_FOUND404allUnknown order, code, SKU, quote or route. Other partners’ data is never visible.
PRICE_STALE409APIQuote expired. details.freshQuote is a new quote.
QUOTE_USED409APIQuote already used by another order.
IDEMPOTENCY_CONFLICT409APIIdempotency-Key reused with a different body.
ALREADY_REVEALED409API · portalCode revealed before. details.revealedAt.
NOT_REVEALABLE409API · portalCode is not revealable (e.g. quarantined or void).
DISPUTE_EXISTS409portalThe code already has an open dispute.
NOT_DISPUTABLE409portalOrder not delivered, or code quarantined/void.
DISPUTE_CLOSED409consoleDispute already credited or rejected.
USER_EXISTS409portal · consoleA user with this email already exists.
WEBHOOK_NOT_CONFIGURED409portalSet a webhook URL before sending a test ping.
SKU_UNAVAILABLE422APISKU cannot be quoted now; message has the reason.
RATE_LIMITED429API · portalReveal rate limit (30/min per partner).
INTERNAL500allOur fault. Safe to retry an order with the same Idempotency-Key.

Idempotency

  • POST /v1/orders requires Idempotency-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.
Retry safely · Node
// 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

ScopeLimitWhen exceeded
/v1/*, /api/*20 requests/s per IP, burst 40 (at the edge)HTTP 429 from the edge
Reveal30 reveals/min per partner429 RATE_LIMITED
Sign-in links10/min per IP, burst 5HTTP 429 from the edge

Back off exponentially on 429. Edge 429s may not carry the JSON error body.

Changelog

  • 2026-09-29 Events dispute.updated and ping. Disputes in the partner portal. Webhook delivery log, retry and test ping in the portal. Wallet statements as CSV.
  • 2026-09 /v1: catalogue, quotes, idempotent orders, single reveal, wallet and ledger, sequenced events and signed webhooks.

Build against the sandbox.

Request access