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.
Authorization: Bearer cp_live_…
A revoked key fails immediately with 401 UNAUTHORIZED.
Conventions
- JSON in and out. Money is a string of integer minor units (paise), in fields ending in
Minor."50000"is ₹500.00. Never parse money as a float. - Timestamps are ISO 8601 strings.
- Errors are
{ "error": { "code", "message", "details"? } }with a 4xx/5xx status. Branch oncode, showmessage. - Plaintext codes appear in exactly one place: the reveal response. Orders, events and webhooks carry masked codes (
****-****-7Q2K) and code ids.
Catalogue
/v1/catalog?region=&brand=live price and availabilityEach 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
/v1/quotes201 → Quote{ "sku": "GPLAY-IN-500", "qty": 2 }
{ "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
/v1/orders201 final · 202 ambiguousSend 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.
POST /v1/orders Idempotency-Key: 6f1c2a52-7d8e-4c1b-9a51-3f0e0c2b9d11 Content-Type: application/json { "quoteId": "…", "clientReference": "txn-8841" }
{ "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 } ] }
/v1/orders/{id}current state, masked codes onlyIf 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
CREATED → WALLET_RESERVED → SUPPLIER_PENDING → FULFILLED | PARTIALLY_FULFILLED | FAILED ↘ SUPPLIER_AMBIGUOUS → (resolver) → same finals
| State | Meaning | Money |
|---|---|---|
| CREATED · WALLET_RESERVED · SUPPLIER_PENDING | Processing. | Reserved. |
| FULFILLED | All codes delivered and sealed. | Charged for all units. |
| PARTIALLY_FULFILLED | Some codes delivered (fewer arrived, or some were quarantined). | Charged for delivered units; the rest released. |
| FAILED | No codes delivered. See failureCode. | Fully released — refunded to available. |
| SUPPLIER_AMBIGUOUS | The 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
/v1/orders/{orderId}/codes/{codeId}/revealRequires 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
/v1/wallet{ currency, availableMinor, reservedMinor }/v1/wallet/ledger?limit=your ledger entries, newest firstBalances 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
/v1/events?since={seq}&limit=oldest first, up to 100Every 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.
| Type | data |
|---|---|
| order.fulfilled | orderId, clientReference, codes[] (masked) |
| order.partial | orderId, clientReference, requested, delivered, refundedMinor, codes[] |
| order.failed | orderId, clientReference, failureCode |
| order.ambiguous | orderId, clientReference |
| code.quarantined | orderId, codeId, reason |
| wallet.credited | amountMinor, 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.
- Delivered in
seqorder per partner. A failed delivery holds later events back, so you never see them out of order. - Retried with exponential backoff up to one hour between attempts. Events are never dropped.
- Respond 2xx within 5 seconds. Handle duplicates by
seq.
Verify a signature in Node.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
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid Bearer key. |
FORBIDDEN | 403 | The key or user may not perform this action. |
PARTNER_INACTIVE | 403 | Your partner account is not ACTIVE (pending, suspended, frozen or closed). |
LIMIT_EXCEEDED | 403 | The order would exceed your daily or monthly tier cap. |
INSUFFICIENT_FUNDS | 402 | Available balance is below the order total. details.availableMinor and details.requiredMinor. |
INVALID_REQUEST | 400 | Body or query failed validation. details describes the fields. |
INVALID_QTY | 400 | qty must be 1–100. |
IDEMPOTENCY_KEY_REQUIRED | 400 | POST /v1/orders needs an Idempotency-Key header (8–128 characters). |
DEVICE_ID_REQUIRED | 400 | Reveal needs an X-Device-Id header. |
SKU_UNAVAILABLE | 422 | The SKU cannot be quoted now; message carries the reason. |
PRICE_STALE | 409 | The quote expired. details.freshQuote is a new quote at the current price — confirm it with a new Idempotency-Key. |
QUOTE_USED | 409 | The quote was already used by another order. |
IDEMPOTENCY_CONFLICT | 409 | The Idempotency-Key was reused with a different body. |
ALREADY_REVEALED | 409 | The code was revealed before. details.revealedAt is the original time. |
NOT_REVEALABLE | 409 | The code is not in a revealable state (e.g. quarantined). |
RATE_LIMITED | 429 | Too many requests — reveals are rate-limited per partner. Back off and retry. |
NOT_FOUND | 404 | Unknown order, code or route. Orders of other partners are never visible. |
INTERNAL | 500 | Our fault. Safe to retry an order with the same Idempotency-Key. |