FRACTURE SHOP API - COMPLETE SETUP FOR DEVELOPERS AND AI AGENTS Guide version: 1.0 | API version: v1 This public document contains no working credentials or license keys. All response IDs, license strings, and shop order references below are examples. 1. IMPLEMENTATION BRIEF FOR YOUR AI AGENT ======================================= Integrate Fracture license fulfillment into the existing shop backend. Inspect the shop's framework, database, payment provider, and fulfillment flow first. Use its existing conventions. Implement server-side API calls, verified payment handling, durable idempotency, authenticated customer delivery, and retry jobs. Do not invent endpoints. The complete OpenAPI contract is appended to this file. Obtain FRACTURE_API_TOKEN from the merchant's private hosting secret manager. If it is missing, identify the exact setting the merchant must configure; never invent a token, put it in source control, or ask for it in a public issue/chat. The merchant creates their own token in Reseller portal > Shop API. This TXT is public and deliberately does not contain their token, password, or account data. Deliver integration code, environment variable names, database migration(s) for durable order mapping/fulfillment, payment webhook setup, and validation results. Use mocked provider/API responses for automated tests. GET balance/plans are read-only live checks. POST /orders creates REAL keys and spends REAL credits; there is no sandbox or dry-run parameter. Arrange any live purchase test with the merchant; do not silently generate live inventory during installation. Use the actual shop's payment provider SDK to verify webhook signatures and payment state. Provider-specific webhook code cannot be inferred from this TXT. Do not deliver keys in response to a browser redirect or an unverified callback. 2. REQUIREMENTS AND BASE URLS ============================ Requirements: an active owner-issued reseller login, an API token, sufficient credits, a server/backend with outbound HTTPS, a durable database or queue, and an authenticated customer delivery mechanism. No Fracture SDK is required. Examples use Node.js 18+ with fetch (a currently supported LTS is recommended), or Python 3.10+ using only standard-library modules. Use HTTPS with certificate verification enabled. No direct access to Fracture's database is needed. Canonical base URL: https://sentinelspoofs.xyz Deployment base URL: https://sentinelspoofs-shop.jinxyware.workers.dev Human documentation: /docs/api Download this guide: /fracture-api-setup.txt Standalone OpenAPI JSON: /openapi.json API token management: /reseller#integration Configure a single base URL in server environment variables. The deployment address serves the same backend/account data if the custom domain is still provisioning. Keep the same idempotency key and payload if changing addresses while recovering an ambiguous request. Never disable TLS verification. Example SERVER environment settings (token value is intentionally omitted): FRACTURE_API_BASE_URL=https://sentinelspoofs.xyz FRACTURE_API_TOKEN= These variables must be server-only. Do not prefix the token with NEXT_PUBLIC_, VITE_, PUBLIC_, or otherwise expose it to browser JavaScript, HTML, analytics, logs, downloadable configuration files, or customer responses. The base URL is public; the token is secret. Do not use Supabase or owner credentials here. 3. CREATE, STORE, AND REVOKE AN API TOKEN ======================================= 1) Sign in with the reseller login issued by the owner. 2) Open Reseller portal > Shop API, enter a recognizable name for this shop, and choose Create API token. Up to 5 active tokens are allowed per account. 3) Save the full token immediately; it is shown once. Only its hash is stored by the service. Use a separate token for each shop. 4) Configure the full token as FRACTURE_API_TOKEN in the backend's secret store. 5) Read balance and plans to check access before wiring paid fulfillment. 6) Revoke a lost/compromised token in the same panel, create a replacement, and update the backend secret. Retrying an existing order with a replacement token on the SAME reseller account still uses that account's idempotency. Token shape: fsk_ followed by 64 lowercase hexadecimal characters. Send the entire value in every API request: Authorization: Bearer Tokens are tied to their reseller account and use its credits and discount. They permit reading balance/catalog and issuing keys. They cannot add credits, create reseller logins, change discounts, or manage other reseller accounts. Owner adds credits manually; arrange top-ups directly with the owner. 4. ENDPOINTS AND WIRE FORMAT ============================ The public Bearer API consists of exactly these three routes: GET /api/v1/balance GET /api/v1/plans POST /api/v1/orders All calls require Authorization. POST additionally requires: Content-Type: application/json Idempotency-Key: JSON request bodies must be objects. /orders accepts at most 8192 UTF-8 BYTES. Rate limits: 60 requests/token/minute AND 120 requests/source-IP/minute. Several shops behind one egress IP share the IP limit. API responses are marked private, no-store. Never cache a response containing keys in a public CDN. GET /api/v1/balance Response HTTP 200: { "balance_credits": 100, "balance_cents": 10000, "discount_percent": 40, "unit": "credits", "credit_scale": 100 } GET /api/v1/plans Response HTTP 200 (example shows one entry; the endpoint returns active plans): { "plans": [{ "id": "temp_1day", "name": "1 Day", "duration": "1day", "duration_secs": 86400, "price_credits": 4.5, "price_cents": 450, "discounted_price_credits": 2.7, "discounted_price_cents": 270 }], "unit": "credits", "credit_scale": 100 } POST /api/v1/orders Example headers: Authorization: Bearer Content-Type: application/json Idempotency-Key: myshop:order:123456:line:1:batch:1 Example body: {"plan_id":"temp_1day","quantity":1} quantity is an INTEGER from 1 through 500, for ONE plan per request. Success HTTP 200 (both newly created orders and successful replays): { "id": "11111111-1111-4111-8111-111111111111", "plan_id": "temp_1day", "quantity": 1, "keys": ["FRACTURE-XXXX-XXXX-XXXX-XXXX"], "unit_price_credits": 2.7, "unit_price_cents": 270, "total_credits": 2.7, "total_cents": 270, "balance_credits": 97.3, "balance_cents": 9730, "replayed": false } id is the Fracture order UUID. keys contains the issued license strings. unit_price_* and total_* are the amount actually charged for this order. balance_* is the account balance reported by the request; concurrent purchases can change it. A replay returns original keys and order charges without another debit, with replayed=true. Do not assume a replay proves a key's current status: keys may subsequently have been redeemed, revoked, or expired. Treat license strings as opaque values; do not derive or construct them locally. 5. PLANS, CREDITS, AND DISCOUNT ROUNDING ====================================== Plan ID Duration Base credits Internal units Duration seconds temp_1day 1 day 4.50 450 86400 temp_3day 3 days 8.00 800 259200 temp_7day 7 days 15.00 1500 604800 temp_30day 30 days 30.00 3000 2592000 temp_lifetime Lifetime 100.00 10000 0 All listed licenses are for the TEMPORARY spoofer product. Lifetime is a license duration, not a permanent spoofing product. Timed licenses start on the first successful loader redemption/binding, not at inventory generation. Newly issued keys are active and initially unbound. Avoid redeeming stock as a setup test. 1 credit = 100 integer precision units. The legacy *_cents fields represent hundredths of a CREDIT, not fiat cents. *_credits fields are decimal credits. The account's owner-assigned discount is an integer from 40% to 75%. Discounted unit units = ceil(base_units * (100 - discount_percent) / 100). Total units = discounted_unit_units * quantity. Round the UNIT price upward to 0.01 credit BEFORE multiplying by quantity. Example: Day at 40% off costs 2.70 credits; Day at 75% off costs 1.13 credits. At 65% off, Day costs 1.58 credits and 100 Day keys cost 158.00 credits. Fetch /plans to show the actual current catalog and discounted prices; do not hardcode a reseller's discount or assume a displayed quote reserves a price. The backend calculates the debit atomically when creating a new order. A replay keeps its original charge. Cash storefront pricing is separate from credits. 6. DURABLE PAYMENT AND FULFILLMENT FLOW ====================================== 1) Maintain a server-controlled mapping of your shop SKU/variant to plan_id. Validate quantity and price on the server; do not accept arbitrary plan IDs, discounts, payment state, or key delivery addresses from browser input. 2) Verify the payment provider's webhook signature using its SDK/raw-body rules. Verify merchant/account, order reference, paid/captured state, amount, and currency against your own saved order (query the provider when appropriate). 3) In a database transaction, persist/deduplicate the provider event and create or find the fulfillment record for that paid order line. Persist immutable plan_id, quantity, and idempotency key BEFORE calling Fracture. 4) Queue fulfillment durably and acknowledge the payment event only after the event/job is durably saved. Keep retries outside a short webhook timeout. 5) Call POST /orders from a backend worker using the saved ID and parameters. 6) Save the returned Fracture order UUID, keys, charge, and completion state in your database before customer delivery. Protect stored keys as sensitive data. 7) Deliver keys only to the authenticated purchaser/order owner. Mark delivery durably and deduplicate email/download events too. A duplicate webhook must recover the same fulfillment record and never purchase another set of keys. 8) On insufficient credits, keep the paid order pending fulfillment and notify the merchant. After a top-up retry the saved request ID. Do not claim delivery occurred or generate a replacement ID merely because fulfillment failed. Suggested unique constraints: provider_event_id; (shop_id, order_id, line_id, batch_number); and fracture_idempotency_key. Useful record fields: plan_id, quantity, payment_verified_at, state, attempts, next_attempt_at, fracture_order_id, encrypted/restricted keys, total_units, last_status, delivered_at, and timestamps. Never log full tokens, keys, or unfiltered response bodies. For multiple plans or more than 500 keys, create deterministic child batches, each with a persisted unique ID and quantity <=500. Track partial completion. Do not assume several API calls are one transaction; recover only pending child batches after failure. Do not silently buy extra keys to replace successful ones. 7. IDEMPOTENCY, RETRIES, AND FAILURES ==================================== Idempotency-Key must be 8-128 characters using letters, digits, _, ., :, or -. Example: myshop:order:123456:line:1:batch:1 Namespace references uniquely across all shops using the same reseller account. If source order IDs have other characters or are too long, persist a deterministic digest/UUID mapping once. Do not generate a fresh UUID on each retry. Same reseller + same request ID + same plan/quantity returns the original keys without spending credits again. Same ID with different parameters returns 409. Idempotency is scoped to the reseller account, not just an individual token. If your server crashes after the API call but before saving its response, retry the EXACT saved ID/body to recover the original order. A timeout or HTTP 5xx does not establish that the first request failed before committing. Standard application error JSON: {"error":"Human-readable message"} Do not depend on exact error wording. Handle status codes. Edge/proxy errors can be HTML, empty, or other non-JSON; parse defensively and do not show raw HTML to customers. Network/TLS timeouts and malformed success responses may be ambiguous. Preserve the ID and queue bounded retries/manual review as needed. 200: Success. Persist and validate the response before delivery. 400: Invalid JSON/fields, unknown/inactive plan, bad quantity or request ID. Correct configuration/data; do not repeatedly retry unchanged bad input. 401: Missing/invalid/revoked token or inactive account. Fix access first. 402: Insufficient credits. Request owner top-up; retry SAME saved ID afterward. 403: Non-reseller API access, or an edge access block. Check configuration/access. 409: ID already used with a different plan/quantity. Reconcile the original saved order; do not work around the conflict by blindly allocating another ID. 413: Request exceeds 8192 bytes. Reduce to the documented body fields. 415: Wrong Content-Type. Send application/json. 429: Rate limit. Wait at least Retry-After seconds (currently 60), then retry the SAME ID with jitter. Coordinate a queue across shops behind one IP. 500/502/503/504 and network failures: Retry with exponential backoff and jitter using the SAME ID/body. Stop after a bounded attempt window and alert the merchant; leave the job recoverable instead of discarding it. Read Retry-After as either seconds or an HTTP date if returned by an intermediary. Never immediately spin on a failed order. A practical queue starts at a few seconds, increases to minutes, and honors any longer Retry-After value. A valid replacement token on the same reseller account may recover existing orders. 8. CURL QUICK START (SERVER SHELL, TOKEN ALREADY SET PRIVATELY) ============================================================ Shell examples below use POSIX syntax. PowerShell users can use curl.exe with the same headers and JSON while adapting environment/line continuation syntax. BASE_URL="${FRACTURE_API_BASE_URL:-https://sentinelspoofs.xyz}" curl --fail-with-body "$BASE_URL/api/v1/balance" \ -H "Authorization: Bearer $FRACTURE_API_TOKEN" curl --fail-with-body "$BASE_URL/api/v1/plans" \ -H "Authorization: Bearer $FRACTURE_API_TOKEN" The next call BUYS one real Day key. Use only for an intended paid fulfillment or an explicitly arranged live test; keep its unique reference for any retry: curl --fail-with-body -X POST "$BASE_URL/api/v1/orders" \ -H "Authorization: Bearer $FRACTURE_API_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: myshop:order:123456:line:1:batch:1' \ -d '{"plan_id":"temp_1day","quantity":1}' 9. NODE.JS SERVER HELPER (NO DEPENDENCIES) ======================================== This helper performs ONE attempt. Your durable worker owns retries and delivery. Only call fulfillSavedOrder with values loaded from a verified paid order record. Do not pass unverified browser parameters directly into it. // fracture-api.mjs const base = (process.env.FRACTURE_API_BASE_URL || 'https://sentinelspoofs.xyz').replace(/\/$/, ''); const token = process.env.FRACTURE_API_TOKEN; if (!token) throw new Error('Configure FRACTURE_API_TOKEN on the server'); if (!base.startsWith('https://')) throw new Error('HTTPS base URL required'); function retryDelay(value) { if (!value) return null; const seconds = Number(value); if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000); const time = Date.parse(value); return Number.isFinite(time) ? Math.max(0, time - Date.now()) : null; } export async function fractureRequest(path, { body, requestId } = {}) { const headers = { Authorization: 'Bearer ' + token, Accept: 'application/json', 'User-Agent': 'Fracture-Shop-Integration/1.0', }; if (body !== undefined) headers['Content-Type'] = 'application/json'; if (requestId) headers['Idempotency-Key'] = requestId; // A fetch/network/timeout exception must be queued with the SAME saved ID. const response = await fetch(base + path, { method: body === undefined ? 'GET' : 'POST', headers, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(30000), cache: 'no-store', redirect: 'error', }); const raw = await response.text(); let data; try { data = JSON.parse(raw); } catch { data = null; } if (!response.ok || !data || typeof data !== 'object') { const error = new Error('Fracture response requires fulfillment review/retry'); error.status = response.status; error.retryAfterMs = retryDelay(response.headers.get('retry-after')); error.retryable = response.ok || response.status === 429 || response.status >= 500; // Preserve status and saved request ID; do not log raw response/token/keys. throw error; } return data; } export const getBalance = () => fractureRequest('/api/v1/balance'); export const getPlans = () => fractureRequest('/api/v1/plans'); export async function fulfillSavedOrder(saved) { if (typeof saved.idempotencyKey !== 'string' || !/^[a-zA-Z0-9_.:-]{8,128}$/.test(saved.idempotencyKey)) { throw new Error('Invalid saved idempotency key'); } if (!Number.isInteger(saved.quantity) || saved.quantity < 1 || saved.quantity > 500) { throw new Error('Invalid saved quantity'); } const result = await fractureRequest('/api/v1/orders', { requestId: saved.idempotencyKey, body: { plan_id: saved.planId, quantity: saved.quantity }, }); if (typeof result.id !== 'string' || result.plan_id !== saved.planId || result.quantity !== saved.quantity || !Array.isArray(result.keys) || result.keys.length !== saved.quantity || result.keys.some(key => typeof key !== 'string' || !key.length)) { const error = new Error('Unexpected order response; retain ID for recovery'); error.retryable = true; throw error; } // Transactionally save result to the paid order before customer delivery. return result; } 10. PYTHON SERVER HELPER (STANDARD LIBRARY) ========================================== This also makes ONE attempt. Persist/reuse request_id in the worker and handle HTTPError status and Retry-After without depending on JSON error bodies. import json import os import re from urllib.error import HTTPError from urllib.request import Request, HTTPRedirectHandler, build_opener class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None # Do not forward Authorization to a redirect destination. OPENER = build_opener(NoRedirect()) BASE = os.environ.get('FRACTURE_API_BASE_URL', 'https://sentinelspoofs.xyz').rstrip('/') TOKEN = os.environ['FRACTURE_API_TOKEN'] if not BASE.startswith('https://'): raise ValueError('HTTPS base URL required') def fulfill_saved_order(request_id, plan_id, quantity): if not re.fullmatch(r'[a-zA-Z0-9_.:-]{8,128}', request_id): raise ValueError('Invalid saved idempotency key') if type(quantity) is not int or not 1 <= quantity <= 500: raise ValueError('Invalid saved quantity') request = Request( BASE + '/api/v1/orders', method='POST', headers={ 'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'Fracture-Shop-Integration/1.0', 'Idempotency-Key': request_id, }, data=json.dumps({'plan_id': plan_id, 'quantity': quantity}).encode('utf-8'), ) try: with OPENER.open(request, timeout=30) as response: raw = response.read() except HTTPError as error: # Safe across JSON/HTML/empty error bodies. Do not log response bodies. failure = RuntimeError('Fracture HTTP ' + str(error.code)) failure.status = error.code failure.retry_after = error.headers.get('Retry-After') failure.retryable = error.code == 429 or error.code >= 500 error.close() raise failure from None # URL/network/timeouts and invalid success JSON can be ambiguous. # The durable worker must retain request_id and retry the same body. result = json.loads(raw) if (not isinstance(result, dict) or not isinstance(result.get('id'), str) or result.get('plan_id') != plan_id or result.get('quantity') != quantity or not isinstance(result.get('keys'), list) or len(result['keys']) != quantity or any(not isinstance(key, str) or not key for key in result['keys'])): raise RuntimeError('Unexpected order response; retain ID for recovery') return result # Persist before delivering to the authenticated buyer. 11. PORTAL, MANUAL DOWNLOADS, AND HWID RESETS =========================================== Resellers can generate 1-500 keys per batch manually in the portal and download them as a TXT file. Existing order batches can also be downloaded there. Your own shop may offer a buyer TXT download: authorize order ownership, use saved fulfillment keys (one per line), send Content-Type text/plain and Content-Disposition attachment, and set Cache-Control private, no-store. Downloading existing keys must NOT call POST /orders with a new request ID. HWID reset is available for keys owned by the reseller in the Licenses panel. It clears the binding and preserves existing redemption/expiry information. There is currently no public Bearer API route for HWID reset, key status lookup, revocation, credit funding, or listing all past orders. Do not invent one or use the reseller's browser session cookie as an API credential. Recover an issued order by replaying its saved ID/body. Keep your own durable order history. 12. IMPLEMENTATION AND ACCEPTANCE CHECKLIST ========================================== [ ] API token exists only in the backend secret store; separate token per shop. [ ] GET balance and plans work; UI distinguishes credits from money. [ ] SKU-to-plan and quantity/amount/currency mapping is controlled on the server. [ ] Payment signature and paid state are verified with the actual provider. [ ] Payment events, order lines, batches and delivery are durably deduplicated. [ ] Saved ID/body is reused for every retry, restart, and duplicate webhook. [ ] Successful fulfillment is stored before the customer receives keys. [ ] Duplicate webhook test results in exactly one purchase and one delivery. [ ] Lost-response/crash test recovers the original keys with the same ID. [ ] Insufficient balance, 401, 409, 429, timeout, HTML 5xx and malformed success responses follow the documented reconciliation/retry rules. [ ] Mixed/>500-key orders track separate deterministic batches and partial state. [ ] Customer TXT/download routes authorize ownership and prohibit shared caching. [ ] Logs redact tokens/license keys; database/backups restrict key access. [ ] A pending job remains visible to the merchant after retry exhaustion. [ ] Agent reports what was implemented, what was tested, and remaining private configuration steps. No live POST test occurs without merchant intent. The following JSON is the full machine-readable API contract from /openapi.json. The downloadable TXT is regenerated from this guide and that contract at build. ============================================================================== FULL OPENAPI 3.1 CONTRACT (JSON) ============================================================================== { "openapi": "3.1.0", "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", "info": { "title": "Fracture Shop Fulfillment API", "version": "1.0.0", "description": "Server-to-server reseller key fulfillment. Create and revoke API tokens in /reseller#integration. The full fsk_ token is shown once and is stored hashed. Credits are added manually by the owner. Current base credit prices before the assigned 40-75% reseller discount are Day 4.5, 3 Days 8, Week 15, Month 30, and Lifetime 100. Decimal *_credits fields are preferred. Legacy *_cents fields are integer hundredths of a credit, not fiat cents. Discounted unit prices are rounded upward to 0.01 credit before multiplication by quantity. Limits are 60 requests per token per minute and 120 per source IP per minute. Use stable paid-shop order references for idempotency. Example IDs and license keys are placeholders." }, "servers": [ { "url": "https://sentinelspoofs.xyz", "description": "Fracture production shop" }, { "url": "https://sentinelspoofs-shop.jinxyware.workers.dev", "description": "Cloudflare deployment address" } ], "externalDocs": { "url": "https://sentinelspoofs.xyz/docs/api", "description": "Integration guide, token creation, retries, and server examples" }, "security": [ { "ResellerBearer": [] } ], "tags": [ { "name": "Credits" }, { "name": "Catalog" }, { "name": "Orders" } ], "paths": { "/api/v1/balance": { "get": { "operationId": "getResellerBalance", "tags": [ "Credits" ], "summary": "Read available credits and discount", "responses": { "200": { "description": "Available reseller credit balance.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Balance" }, "example": { "balance_credits": 100, "balance_cents": 10000, "discount_percent": 40, "unit": "credits", "credit_scale": 100 } } } }, "401": { "description": "Missing, invalid, or revoked Bearer token, or inactive account.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Missing, invalid, or revoked Bearer token, or inactive account." } } } }, "403": { "description": "The account is not eligible for reseller API access.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "The account is not eligible for reseller API access." } } } }, "429": { "description": "Rate limit exceeded. Retry with the same order idempotency key when applicable.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" }, "Retry-After": { "schema": { "type": "string", "enum": [ "60" ] }, "description": "Wait at least 60 seconds before retrying." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Rate limit exceeded. Retry with the same order idempotency key when applicable." } } } }, "500": { "description": "Unexpected service error. Retain the order reference when retrying.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Unexpected service error. Retain the order reference when retrying." } } } }, "503": { "description": "Temporary service failure. Retry with backoff and the same order idempotency key when applicable.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Temporary service failure. Retry with backoff and the same order idempotency key when applicable." } } } } } } }, "/api/v1/plans": { "get": { "operationId": "getResellerPlans", "tags": [ "Catalog" ], "summary": "Read active plans and discounted credit prices", "description": "Read this endpoint to discover current plan IDs and prices for the authenticated reseller. The sample contains one plan; the real response contains all active plans.", "responses": { "200": { "description": "Active plan catalog with reseller pricing.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plans" }, "example": { "plans": [ { "id": "temp_1day", "name": "1 Day", "duration": "1day", "duration_secs": 86400, "price_credits": 4.5, "price_cents": 450, "discounted_price_credits": 2.7, "discounted_price_cents": 270 } ], "unit": "credits", "credit_scale": 100 } } } }, "401": { "description": "Missing, invalid, or revoked Bearer token, or inactive account.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Missing, invalid, or revoked Bearer token, or inactive account." } } } }, "403": { "description": "The account is not eligible for reseller API access.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "The account is not eligible for reseller API access." } } } }, "429": { "description": "Rate limit exceeded. Retry with the same order idempotency key when applicable.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" }, "Retry-After": { "schema": { "type": "string", "enum": [ "60" ] }, "description": "Wait at least 60 seconds before retrying." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Rate limit exceeded. Retry with the same order idempotency key when applicable." } } } }, "500": { "description": "Unexpected service error. Retain the order reference when retrying.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Unexpected service error. Retain the order reference when retrying." } } } }, "503": { "description": "Temporary service failure. Retry with backoff and the same order idempotency key when applicable.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Temporary service failure. Retry with backoff and the same order idempotency key when applicable." } } } } } } }, "/api/v1/orders": { "post": { "operationId": "createResellerOrder", "tags": [ "Orders" ], "summary": "Issue keys and debit credits atomically", "description": "Call from your shop server after verifying the payment event and paid state. The server resolves the account, price, and discount from the token. Use a stable Idempotency-Key derived from the paid shop order. Repeating the same key, plan, and quantity returns HTTP 200 with the original keys and replayed=true without another debit. Reusing it for different inputs returns HTTP 409. On ambiguous timeouts and temporary failures, retry the same request with the same key. Persist the returned order ID and keys and make your own customer delivery idempotent.", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": true, "description": "Stable unique server-side shop order reference. Preserve through retries. 8-128 letters, digits, underscores, dots, colons, or hyphens.", "schema": { "type": "string", "minLength": 8, "maxLength": 128, "pattern": "^[a-zA-Z0-9_.:-]{8,128}$" }, "example": "myshop:order:123456" } ], "requestBody": { "required": true, "description": "JSON body must be no more than 8,192 bytes. Plan and quantity are selected by the shop server after payment verification.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrderRequest" }, "example": { "plan_id": "temp_1day", "quantity": 1 } } } }, "responses": { "200": { "description": "Keys issued, or the original matching order replayed with no additional debit.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" }, "example": { "id": "11111111-1111-4111-8111-111111111111", "plan_id": "temp_1day", "quantity": 1, "keys": [ "FRACTURE-XXXX-XXXX-XXXX-XXXX" ], "unit_price_credits": 2.7, "unit_price_cents": 270, "total_credits": 2.7, "total_cents": 270, "balance_credits": 97.3, "balance_cents": 9730, "replayed": false } } } }, "400": { "description": "Invalid plan, quantity, or idempotency header.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Invalid plan, quantity, or idempotency header." } } } }, "401": { "description": "Missing, invalid, or revoked Bearer token, or inactive account.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Missing, invalid, or revoked Bearer token, or inactive account." } } } }, "402": { "description": "Insufficient reseller credits. Ask the owner for a manual top-up, then retry the same order.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Insufficient reseller credits. Ask the owner for a manual top-up, then retry the same order." } } } }, "403": { "description": "The account is not eligible for reseller API access.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "The account is not eligible for reseller API access." } } } }, "409": { "description": "The idempotency key was previously used with a different plan or quantity.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "The idempotency key was previously used with a different plan or quantity." } } } }, "413": { "description": "Request body exceeds 8,192 bytes.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Request body exceeds 8,192 bytes." } } } }, "415": { "description": "Content-Type must be application/json.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Content-Type must be application/json." } } } }, "429": { "description": "Rate limit exceeded. Retry with the same order idempotency key when applicable.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" }, "Retry-After": { "schema": { "type": "string", "enum": [ "60" ] }, "description": "Wait at least 60 seconds before retrying." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Rate limit exceeded. Retry with the same order idempotency key when applicable." } } } }, "500": { "description": "Unexpected service error. Retain the order reference when retrying.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Unexpected service error. Retain the order reference when retrying." } } } }, "503": { "description": "Temporary service failure. Retry with backoff and the same order idempotency key when applicable.", "headers": { "Cache-Control": { "schema": { "type": "string" }, "description": "private, no-store" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "Temporary service failure. Retry with backoff and the same order idempotency key when applicable." } } } } } } } }, "components": { "securitySchemes": { "ResellerBearer": { "type": "http", "scheme": "bearer", "bearerFormat": "fsk_ followed by 64 lowercase hexadecimal characters", "description": "Create a token in the authenticated reseller portal. Send Authorization: Bearer YOUR_TOKEN. Store it in the shop server environment. Tokens authorize their owning reseller account only." } }, "schemas": { "Error": { "type": "object", "properties": { "error": { "type": "string" } }, "required": [ "error" ] }, "Balance": { "type": "object", "required": [ "balance_credits", "balance_cents", "discount_percent", "unit", "credit_scale" ], "properties": { "balance_credits": { "type": "number", "minimum": 0, "multipleOf": 0.01, "description": "Decimal credits. One credit has 100 precision units." }, "balance_cents": { "type": "integer", "minimum": 0, "description": "Legacy hundredths of one credit. Divide by 100 for credits." }, "discount_percent": { "type": "integer", "minimum": 40, "maximum": 75 }, "unit": { "type": "string", "const": "credits" }, "credit_scale": { "type": "integer", "const": 100 } } }, "Plan": { "type": "object", "required": [ "id", "name", "duration", "duration_secs", "price_credits", "price_cents", "discounted_price_credits", "discounted_price_cents" ], "properties": { "id": { "type": "string", "example": "temp_1day" }, "name": { "type": "string", "example": "1 Day" }, "duration": { "type": "string", "example": "1day" }, "duration_secs": { "type": "integer", "minimum": 0, "description": "Zero denotes the Lifetime temporary-access license." }, "price_credits": { "type": "number", "minimum": 0, "multipleOf": 0.01, "description": "Base credit price before the reseller discount." }, "price_cents": { "type": "integer", "minimum": 0, "description": "Legacy hundredths of one credit. Divide by 100 for credits." }, "discounted_price_credits": { "type": "number", "minimum": 0, "multipleOf": 0.01, "description": "Your discounted credit price per key, rounded upward to 0.01 credit." }, "discounted_price_cents": { "type": "integer", "minimum": 0, "description": "Legacy hundredths of one credit. Divide by 100 for credits." } } }, "Plans": { "type": "object", "required": [ "plans", "unit", "credit_scale" ], "properties": { "plans": { "type": "array", "items": { "$ref": "#/components/schemas/Plan" } }, "unit": { "type": "string", "const": "credits" }, "credit_scale": { "type": "integer", "const": 100 } } }, "OrderRequest": { "type": "object", "required": [ "plan_id", "quantity" ], "properties": { "plan_id": { "type": "string", "minLength": 1, "maxLength": 50, "description": "An active plan ID returned by the plans endpoint.", "example": "temp_1day" }, "quantity": { "type": "integer", "minimum": 1, "maximum": 500 } } }, "Order": { "type": "object", "required": [ "id", "plan_id", "quantity", "keys", "unit_price_credits", "unit_price_cents", "total_credits", "total_cents", "balance_credits", "balance_cents", "replayed" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Fracture order ID. Persist it with your paid shop order." }, "plan_id": { "type": "string", "example": "temp_1day" }, "quantity": { "type": "integer", "minimum": 1, "maximum": 500 }, "keys": { "type": "array", "minItems": 1, "maxItems": 500, "items": { "type": "string", "pattern": "^FRACTURE-[A-HJ-NP-Z2-9]{4}(?:-[A-HJ-NP-Z2-9]{4}){3}$" } }, "unit_price_credits": { "type": "number", "minimum": 0, "multipleOf": 0.01, "description": "Decimal credits. One credit has 100 precision units." }, "unit_price_cents": { "type": "integer", "minimum": 0, "description": "Legacy hundredths of one credit. Divide by 100 for credits." }, "total_credits": { "type": "number", "minimum": 0, "multipleOf": 0.01, "description": "Decimal credits. One credit has 100 precision units." }, "total_cents": { "type": "integer", "minimum": 0, "description": "Legacy hundredths of one credit. Divide by 100 for credits." }, "balance_credits": { "type": "number", "minimum": 0, "multipleOf": 0.01, "description": "Current balance at response time. It may differ on a later replay." }, "balance_cents": { "type": "integer", "minimum": 0, "description": "Legacy hundredths of one credit. Divide by 100 for credits." }, "replayed": { "type": "boolean", "description": "True when the original order is returned without another debit." } } } } } }