{"openapi":"3.0.3","info":{"title":"RS COIN API","version":"1.0.0","description":"Crossmint-backed credential issuance API for RS COIN dual-chain infrastructure. Credential NFTs are minted on Ethereum (Sepolia) via Crossmint. All routes live under `/api`.","contact":{"email":"developer@braav.co"}},"servers":[{"url":"/api","description":"Same-origin (Next.js API routes)"}],"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","description":"Pass `API_KEY` as a Bearer token **or** via `x-api-key` header."},"AdminKey":{"type":"apiKey","in":"header","name":"x-admin-key","description":"Pass `ADMIN_API_KEY` in the `x-admin-key` header (or as Bearer token)."},"CronSecret":{"type":"apiKey","in":"header","name":"x-cron-secret","description":"Pass `CRON_SECRET` in `x-cron-secret` header or as `?secret=` query param."}},"schemas":{"SuccessEnvelope":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","example":true},"data":{"type":"object"}}},"ErrorEnvelope":{"type":"object","required":["success","error"],"properties":{"success":{"type":"boolean","example":false},"error":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}}},"Tier":{"type":"string","enum":["BASIC","PRO","ENTERPRISE"],"description":"Credential tier."},"PaymentStatus":{"type":"string","enum":["pending","completed","failed","cancelled","timed_out"]},"CredentialStatus":{"type":"string","enum":["ACTIVE","REVOKED","EXPIRED"]},"PaySession":{"type":"object","required":["sessionId","checkoutUrl","stub"],"properties":{"sessionId":{"type":"string","example":"abc123xyz"},"checkoutUrl":{"type":"string","example":"/pay/stub-checkout/abc123xyz"},"stub":{"type":"boolean","description":"True when `STUB_PAY=true` — local stub checkout, no live payment provider called."}}},"PayStatus":{"type":"object","required":["sessionId","status","stub"],"properties":{"sessionId":{"type":"string"},"status":{"$ref":"#/components/schemas/PaymentStatus"},"checkoutUrl":{"type":"string","format":"uri"},"stub":{"type":"boolean"}}},"Credential":{"type":"object","required":["id","paymentId","walletAddress","tier","status","grantedAt"],"properties":{"id":{"type":"string"},"paymentId":{"type":"string"},"walletAddress":{"type":"string","example":"0xAbCd…1234"},"tier":{"$ref":"#/components/schemas/Tier"},"nftTokenId":{"type":"string","nullable":true},"txHash":{"type":"string","nullable":true},"status":{"$ref":"#/components/schemas/CredentialStatus"},"expiresAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"revokedReason":{"type":"string","nullable":true},"grantedAt":{"type":"string","format":"date-time"}}},"AuditLog":{"type":"object","required":["id","createdAt","actor","action","entityType","entityId"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"actor":{"type":"string","example":"admin"},"action":{"type":"string","example":"credential_revoke"},"entityType":{"type":"string","example":"credential"},"entityId":{"type":"string"},"reason":{"type":"string","nullable":true},"before":{"type":"object","nullable":true},"after":{"type":"object","nullable":true}}},"Payment":{"type":"object","required":["id","sessionId","userId","walletAddress","tier","amount","currency","status","createdAt","updatedAt"],"properties":{"id":{"type":"string"},"sessionId":{"type":"string"},"userId":{"type":"string"},"walletAddress":{"type":"string"},"tier":{"$ref":"#/components/schemas/Tier"},"amount":{"type":"number","example":99.99},"currency":{"type":"string","example":"USD"},"status":{"$ref":"#/components/schemas/PaymentStatus"},"checkoutUrl":{"type":"string","format":"uri","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DashboardStats":{"type":"object","required":["credentials","payments","recentActivity"],"properties":{"credentials":{"type":"object","properties":{"total":{"type":"integer"},"active":{"type":"integer"},"revoked":{"type":"integer"}}},"payments":{"type":"object","properties":{"total":{"type":"integer"},"completed":{"type":"integer"},"pending":{"type":"integer"}}},"recentActivity":{"type":"array","items":{"$ref":"#/components/schemas/AuditLog"}}}},"PaginationMeta":{"type":"object","required":["total","page","limit","pages"],"properties":{"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"success":false,"error":{"message":"Unauthorized"}}}}},"NotFound":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"success":false,"error":{"message":"Not found"}}}}},"BadRequest":{"description":"Validation error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"parameters":{"PageParam":{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},"LimitParam":{"name":"limit","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}}}},"tags":[{"name":"Health","description":"System health check"},{"name":"Pay","description":"Payment session lifecycle"},{"name":"Credential","description":"Public credential lookup"},{"name":"Admin – Credentials","description":"Admin credential management"},{"name":"Admin – Payments","description":"Admin payment records"},{"name":"Admin – Stats","description":"Dashboard statistics"},{"name":"Cron","description":"Scheduled reconciliation job"},{"name":"Webhook","description":"Crossmint inbound webhook"},{"name":"Reserve","description":"Reserve Engine integration surface (interfaces only; engine arrives in M3)"}],"paths":{"/reserve/status":{"get":{"tags":["Reserve"],"summary":"Reserve status","description":"Relays whatever the registered reserve engine reports. Until an engine is wired (M3), returns `provider: not_configured` with empty metrics. No reserve logic.","operationId":"reserveStatus","responses":{"200":{"description":"Reserve status (provider, engine, metrics, notices)"},"401":{"description":"Unauthorized"}}}},"/reserve/config":{"get":{"tags":["Reserve"],"summary":"Reserve integration config (admin)","description":"Configuration ABOUT the integration (enabled, endpoint) — never reserve policy.","operationId":"reserveConfig","responses":{"200":{"description":"Integration config"},"401":{"description":"Unauthorized"}}}},"/reserve/health":{"get":{"tags":["Reserve"],"summary":"Reserve engine liveness probe","description":"Monitoring placeholder. Unauthenticated. Null provider reports healthy when no engine is wired.","operationId":"reserveHealth","security":[],"responses":{"200":{"description":"Healthy"},"503":{"description":"Engine unhealthy"}}}},"/reserve/events":{"post":{"tags":["Reserve"],"summary":"Emit a platform event to the reserve engine (admin)","description":"Event hook. Accepts payment.completed | credential.issued | credential.revoked | founder.issuance. Fire-and-forget — a reserve engine can never block the caller. Inert until an engine is registered.","operationId":"reserveEvent","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["payment.completed","credential.issued","credential.revoked","founder.issuance"]},"ref":{"type":"string"},"data":{"type":"object"}}}}}},"responses":{"200":{"description":"Accepted"},"400":{"description":"Bad event"},"401":{"description":"Unauthorized"}}}},"/health":{"get":{"tags":["Health"],"summary":"Health check","description":"Pings the database and returns `ok: true` if healthy.","operationId":"healthCheck","security":[],"responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"ok":{"type":"boolean"},"db":{"type":"string","example":"connected"}}}}}}}},"503":{"description":"Database unreachable"}}}},"/pay/create":{"post":{"tags":["Pay"],"summary":"Create payment session","description":"Creates a payment session and returns a `checkoutUrl`. When `STUB_PAY=true` it's a local stub checkout; otherwise it points to the embedded Crossmint checkout page, which creates the order client-side. Crossmint mints + delivers the credential and the `orders.delivery.completed` webhook records it (correlated to this session by wallet).","operationId":"createPaySession","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId","walletAddress","amount","tier"],"properties":{"userId":{"type":"string","example":"user_abc123"},"walletAddress":{"type":"string","example":"0xAbCd…1234"},"amount":{"type":"number","example":99.99},"tier":{"$ref":"#/components/schemas/Tier"}}}}}},"responses":{"201":{"description":"Session created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/PaySession"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/pay/status/{sessionId}":{"get":{"tags":["Pay"],"summary":"Get payment status","description":"Returns the current stored status of a payment session. Live provider polling returns in Phase B.","operationId":"getPayStatus","security":[{"ApiKey":[]}],"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"},"example":"abc123xyz"}],"responses":{"200":{"description":"Session status","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/PayStatus"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/pay/reconcile/{sessionId}":{"post":{"tags":["Pay"],"summary":"Reconcile a session","description":"Reconciles a session against the live payment provider and—if completed—triggers idempotent credential issuance. Live provider polling returns in Phase B; currently returns the stored status.","operationId":"reconcileSession","security":[{"ApiKey":[]}],"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reconcile result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"sessionId":{"type":"string"},"status":{"$ref":"#/components/schemas/PaymentStatus"},"credentialIssued":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/pay/simulate-complete/{sessionId}":{"post":{"tags":["Pay"],"summary":"Simulate payment completion (stub only)","description":"Only works when `STUB_PAY=true` (returns 403 otherwise). Marks the session as completed and issues the credential via Crossmint, mimicking a real payment callback.","operationId":"simulateComplete","security":[{"ApiKey":[]}],"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Simulated completion result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"sessionId":{"type":"string"},"credentialId":{"type":"string"},"nftTokenId":{"type":"string"}}}}}}}},"400":{"description":"Not in stub mode or session already completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/credential/check/{wallet}":{"get":{"tags":["Credential"],"summary":"Check credentials for wallet","description":"Returns all credentials (active and revoked) associated with a wallet address.","operationId":"checkCredential","security":[{"ApiKey":[]}],"parameters":[{"name":"wallet","in":"path","required":true,"description":"EVM wallet address (case-insensitive).","schema":{"type":"string","example":"0xAbCd…1234"}}],"responses":{"200":{"description":"Credential list","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Credential"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/admin/stats":{"get":{"tags":["Admin – Stats"],"summary":"Dashboard statistics","description":"Returns credential and payment counts plus the 20 most recent audit events.","operationId":"adminStats","security":[{"AdminKey":[]}],"responses":{"200":{"description":"Dashboard stats","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/DashboardStats"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/admin/credentials":{"get":{"tags":["Admin – Credentials"],"summary":"List credentials","description":"Paginated list of all credentials with optional filters.","operationId":"listCredentials","security":[{"AdminKey":[]}],"parameters":[{"$ref":"#/components/parameters/PageParam"},{"$ref":"#/components/parameters/LimitParam"},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/CredentialStatus"},"description":"Filter by status."},{"name":"tier","in":"query","schema":{"$ref":"#/components/schemas/Tier"},"description":"Filter by tier."},{"name":"wallet","in":"query","schema":{"type":"string"},"description":"Filter by wallet address (partial match)."}],"responses":{"200":{"description":"Paginated credentials","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"allOf":[{"$ref":"#/components/schemas/PaginationMeta"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Credential"}}}}]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/admin/credentials/{id}":{"get":{"tags":["Admin – Credentials"],"summary":"Get credential detail","description":"Returns the credential plus its full audit trail.","operationId":"getCredential","security":[{"AdminKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Credential with audit trail","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"credential":{"$ref":"#/components/schemas/Credential"},"auditTrail":{"type":"array","items":{"$ref":"#/components/schemas/AuditLog"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/admin/credentials/{id}/revoke":{"post":{"tags":["Admin – Credentials"],"summary":"Revoke credential","operationId":"revokeCredential","security":[{"AdminKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":3,"example":"Policy violation"}}}}}},"responses":{"200":{"description":"Credential revoked","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Credential"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/admin/credentials/{id}/extend":{"post":{"tags":["Admin – Credentials"],"summary":"Extend credential expiry","operationId":"extendCredential","security":[{"AdminKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["expiresAt"],"properties":{"expiresAt":{"type":"string","format":"date-time","example":"2027-01-01T00:00:00Z"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Credential extended","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Credential"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/admin/credentials/{id}/upgrade":{"post":{"tags":["Admin – Credentials"],"summary":"Upgrade credential tier","operationId":"upgradeCredential","security":[{"AdminKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tier"],"properties":{"tier":{"$ref":"#/components/schemas/Tier"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Credential upgraded","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Credential"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/admin/credentials/{id}/reissue":{"post":{"tags":["Admin – Credentials"],"summary":"Reissue credential NFT","description":"Burns the old NFT token and mints a fresh one, updating the `nftTokenId` on the credential record.","operationId":"reissueCredential","security":[{"AdminKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Credential reissued","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Credential"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/admin/payments":{"get":{"tags":["Admin – Payments"],"summary":"List payments","description":"Paginated list of all payment sessions with optional status / search filters.","operationId":"listPayments","security":[{"AdminKey":[]}],"parameters":[{"$ref":"#/components/parameters/PageParam"},{"$ref":"#/components/parameters/LimitParam"},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/PaymentStatus"}},{"name":"search","in":"query","schema":{"type":"string"},"description":"Partial match on userId, walletAddress, or sessionId."}],"responses":{"200":{"description":"Paginated payments","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"allOf":[{"$ref":"#/components/schemas/PaginationMeta"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}}}}]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/admin/payments/{id}":{"get":{"tags":["Admin – Payments"],"summary":"Get payment detail","description":"Returns the payment, its linked credential (if any), and the audit trail.","operationId":"getPayment","security":[{"AdminKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payment detail","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"payment":{"$ref":"#/components/schemas/Payment"},"credential":{"oneOf":[{"$ref":"#/components/schemas/Credential"},{"type":"null"}]},"auditTrail":{"type":"array","items":{"$ref":"#/components/schemas/AuditLog"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/webhook/crossmint":{"post":{"tags":["Webhook"],"summary":"Crossmint checkout webhook","description":"Receives Crossmint Checkout V3 order events (`orders.payment.succeeded`, `orders.delivery.completed`, etc.). Verified via Svix signature headers (`svix-id`, `svix-timestamp`, `svix-signature`) using `CROSSMINT_WEBHOOK_SECRET`. On `orders.delivery.completed` the delivered credential is recorded idempotently. Responds `200 { ok: true }` and processes async.","operationId":"crossmintWebhook","security":[],"parameters":[{"name":"svix-id","in":"header","required":true,"schema":{"type":"string"}},{"name":"svix-timestamp","in":"header","required":true,"schema":{"type":"string"}},{"name":"svix-signature","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","data"],"properties":{"type":{"type":"string","example":"orders.delivery.completed"},"data":{"type":"object"}}}}}},"responses":{"200":{"description":"Acknowledged (check `ok`).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Invalid signature or body."}}}},"/cron/reconcile":{"get":{"tags":["Cron"],"summary":"Reconcile pending sessions","description":"Reconciles non-terminal payment sessions against the live payment provider and issues credentials for newly-completed ones. Live provider polling returns in Phase B of the Crossmint migration; currently a no-op. Protected by `CRON_SECRET`. Not scheduled by default — trigger externally or add a `crons` entry to `vercel.json`.","operationId":"cronReconcile","security":[{"CronSecret":[]}],"responses":{"200":{"description":"Reconcile summary","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"processed":{"type":"integer"},"stub":{"type":"boolean","description":"True when running in STUB_PAY mode."},"errors":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}}}