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

API reference

Quote, order with an Idempotency-Key, reveal each sealed code exactly once. Sequenced, HMAC-signed events you can replay from any point.

Overview

The Codeproof partner API sells prepaid brand codes from your wallet: get a quote, place an order with an Idempotency-Key, and reveal each sealed code exactly once. Events are sequenced per partner and delivered as HMAC-signed webhooks, and can be replayed from any sequence number.

Base URL https://codeproof.fluxusforge.in. Codeproof is in build; keys are issued to invited partners, and a sandbox deployment runs against a simulated supplier. Partner-portal users can create keys under Developers.

Authentication

Send your API key as a Bearer token on every request. Keys are shown once at creation and stored by us only as a hash. Keep them on your servers — never in a mobile app or browser.

Header
Authorization: Bearer cp_live_…

A revoked key fails immediately with 401 UNAUTHORIZED.

Conventions

Catalogue

GET/v1/catalog?region=&brand=live price and availability

Each item has sku, brand, product, region, currency, faceValueMinor, available and, when available, your unitPriceMinor. Unavailable items carry an unavailableReason (for example stale FX, expiry under 60 days, margin floor, or no resale rights).

Quotes

POST/v1/quotes201 → Quote
Request
{ "sku": "GPLAY-IN-500", "qty": 2 }
Response
{
  "quoteId": "…",
  "sku": "GPLAY-IN-500",
  "qty": 2,
  "unitPriceMinor": "48750",
  "totalMinor": "97500",
  "currency": "INR",
  "expiresAt": "2026-10-01T10:15:00.000Z"
}

qty is 1–100. The price holds until expiresAt (about a minute by default). A quote can be used by one order only.

Orders

POST/v1/orders201 final · 202 ambiguous

Send a unique Idempotency-Key (8–128 characters; a UUID per checkout attempt works well). If the network drops, retry with the same key and body: you get the original result and no second purchase. Reusing a key with a different body returns 409 IDEMPOTENCY_CONFLICT.

Request
POST /v1/orders
Idempotency-Key: 6f1c2a52-7d8e-4c1b-9a51-3f0e0c2b9d11
Content-Type: application/json

{ "quoteId": "…", "clientReference": "txn-8841" }
Response · 201
{
  "orderId": "…",
  "state": "FULFILLED",
  "sku": "GPLAY-IN-500",
  "qty": 2, "deliveredQty": 2, "quarantinedQty": 0,
  "unitPriceMinor": "48750", "chargedMinor": "97500", "currency": "INR",
  "deliveryMode": "API", "clientReference": "txn-8841", "failureCode": null,
  "createdAt": "…",
  "codes": [
    { "codeId": "…", "masked": "****-****-7Q2K", "status": "DELIVERED", "expiresAt": "…", "revealedAt": null }
  ]
}
GET/v1/orders/{id}current state, masked codes only

If the quote expired you get 409 PRICE_STALE with details.freshQuote. Show the new price to your user and confirm it with a new Idempotency-Key.

Order states

State machine
CREATED → WALLET_RESERVED → SUPPLIER_PENDING → FULFILLED | PARTIALLY_FULFILLED | FAILED
                                            ↘ SUPPLIER_AMBIGUOUS → (resolver) → same finals
StateMeaningMoney
CREATED · WALLET_RESERVED · SUPPLIER_PENDINGProcessing.Reserved.
FULFILLEDAll codes delivered and sealed.Charged for all units.
PARTIALLY_FULFILLEDSome codes delivered (fewer arrived, or some were quarantined).Charged for delivered units; the rest released.
FAILEDNo codes delivered. See failureCode.Fully released — refunded to available.
SUPPLIER_AMBIGUOUSThe supplier did not answer definitively. We never retry the purchase; a resolver asks the supplier about our reference and settles it.Held until resolved.

Reveal a code

POST/v1/orders/{orderId}/codes/{codeId}/reveal

Requires X-Device-Id — a stable identifier of the device or terminal doing the reveal, recorded in the audit trail. Returns { codeId, code, revealedAt } once. A second call returns 409 ALREADY_REVEALED with the original details.revealedAt. Reveals are rate-limited per partner.

Do not store the plaintext longer than you must, and never log it. Hand it to the end user and drop it.

Wallet

GET/v1/wallet{ currency, availableMinor, reservedMinor }
GET/v1/wallet/ledger?limit=your ledger entries, newest first

Balances are sums of ledger postings. Top-ups arrive by bank transfer to your virtual account and are credited against the UTR — the same UTR is never credited twice. A credit emits wallet.credited.

Events

GET/v1/events?since={seq}&limit=oldest first, up to 100

Every event has a per-partner, strictly increasing seq. Store the last seq you processed and pull from there to catch up after downtime — the same stream your webhook receives.

Typedata
order.fulfilledorderId, clientReference, codes[] (masked)
order.partialorderId, clientReference, requested, delivered, refundedMinor, codes[]
order.failedorderId, clientReference, failureCode
order.ambiguousorderId, clientReference
code.quarantinedorderId, codeId, reason
wallet.creditedamountMinor, utr

Webhooks

Set an https URL in the partner portal; you get a signing secret once. We POST each event as JSON with headers X-Codeproof-Event, X-Codeproof-Seq and X-Codeproof-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of `${t}.${rawBody}` with your secret.

Verify a signature in Node.js

webhook.js
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.CODEPROOF_WEBHOOK_SECRET; // shown once when you set the URL
const TOLERANCE_S = 300;

// Verify against 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('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1 ?? '', 'hex');
  const fresh = Math.abs(Date.now() / 1000 - t) <= TOLERANCE_S;

  if (!fresh || a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(400).send('bad signature');
  }

  const event = JSON.parse(body); // { seq, type, createdAt, data }
  // Idempotent handling: store event.seq; skip if already processed.
  res.sendStatus(200); // any 2xx acknowledges; anything else is retried
});

Error codes

CodeHTTPMeaning
UNAUTHORIZED401Missing or invalid Bearer key.
FORBIDDEN403The key or user may not perform this action.
PARTNER_INACTIVE403Your partner account is not ACTIVE (pending, suspended, frozen or closed).
LIMIT_EXCEEDED403The order would exceed your daily or monthly tier cap.
INSUFFICIENT_FUNDS402Available balance is below the order total. details.availableMinor and details.requiredMinor.
INVALID_REQUEST400Body or query failed validation. details describes the fields.
INVALID_QTY400qty must be 1–100.
IDEMPOTENCY_KEY_REQUIRED400POST /v1/orders needs an Idempotency-Key header (8–128 characters).
DEVICE_ID_REQUIRED400Reveal needs an X-Device-Id header.
SKU_UNAVAILABLE422The SKU cannot be quoted now; message carries the reason.
PRICE_STALE409The quote expired. details.freshQuote is a new quote at the current price — confirm it with a new Idempotency-Key.
QUOTE_USED409The quote was already used by another order.
IDEMPOTENCY_CONFLICT409The Idempotency-Key was reused with a different body.
ALREADY_REVEALED409The code was revealed before. details.revealedAt is the original time.
NOT_REVEALABLE409The code is not in a revealable state (e.g. quarantined).
RATE_LIMITED429Too many requests — reveals are rate-limited per partner. Back off and retry.
NOT_FOUND404Unknown order, code or route. Orders of other partners are never visible.
INTERNAL500Our fault. Safe to retry an order with the same Idempotency-Key.

Build against the sandbox.

Request access