UNICUS · 开发者文档
一期 API 契约 v0.4
所有业务路径按组织成员实时鉴权。参考估价不可直接下单;生产报价必须有效且在余额最低值之上预占法币。
openapi: 3.1.0
info:
title: UNICUS API
version: 0.4.0
description: Organization-scoped一期 contract. Upstream adapters and production status remain explicitly simulated until partner test credentials are verified.
servers:
- url: https://api.unicus.fun
description: Planned partner endpoint; verify DNS and HTTPS before customer use.
paths:
/v1/tasks:
post:
summary: Create an asynchronous task
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/TaskRequest' }
responses:
'201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/Task' } } } }
'200': { description: Existing idempotent task returned }
'409': { description: Idempotency payload conflict }
/v1/tasks/{id}:
get:
summary: Read a task
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '200': { description: Task }, '404': { description: Not found } }
/v1/tasks/{id}/attempts:
get:
summary: Read durable task attempts
description: Organization-scoped attempt history records worker retries and terminal errors; it never creates a second task.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '200': { description: Attempt history }, '404': { description: Not found } }
/v1/tasks/{id}/callbacks:
get:
summary: Read task callback delivery log
description: Organization-scoped readback of durable callback delivery attempts. Reading this log never retries or changes the task result.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '200': { description: Callback deliveries }, '404': { description: Not found } }
/v1/tasks/{id}/events:
post:
summary: Apply one durable task event
description: Event IDs are organization scoped. Replays are no-ops and late lower-rank states cannot overwrite terminal results.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }, { $ref: '#/components/parameters/WebhookSignature' }]
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/EventRequest' } } }
responses: { '202': { description: Event applied }, '200': { description: Event replay }, '404': { description: Not found }, '409': { description: Event ID fingerprint conflict } }
/v1/internal/organizations/{organizationId}/callbacks/{id}/redeliver:
post:
summary: Operator-only callback redelivery
description: Resets a failed or pending customer callback outbox row. Requires the deployment operator token and never changes the underlying task result.
parameters:
- { name: organizationId, in: path, required: true, schema: { type: string } }
- { name: id, in: path, required: true, schema: { type: string } }
responses: { '202': { description: Callback queued }, '401': { description: Operator token required }, '404': { description: Not found } }
/v1/quotes:
post:
summary: Create a reference or production quote
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/QuoteRequest' } } }
responses: { '201': { description: Quote } }
/v1/quotes/{id}:
get:
summary: Read a quote
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '200': { description: Quote }, '404': { description: Not found } }
/v1/orders:
post:
summary: Place an order from a valid production quote
description: Reference estimates cannot be ordered. Fiat is reserved atomically and cannot go below the configured minimum (default zero).
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/OrderRequest' } } }
responses: { '201': { description: Order }, '409': { description: Quote expired or insufficient balance } }
/v1/orders/{id}:
get:
summary: Read an order
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '200': { description: Order }, '404': { description: Not found } }
/v1/orders/{id}/callbacks:
get:
summary: Read order callback delivery log
description: Organization-scoped readback of durable order callback delivery attempts. Reading this log never retries or changes the order result.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '200': { description: Callback deliveries }, '404': { description: Not found } }
/v1/orders/{id}/events:
post:
summary: Apply a durable production or logistics event
description: Organization-scoped event IDs are idempotent; order statuses advance monotonically and terminal conflicts are rejected.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/OrderEventRequest' } } } }
responses: { '202': { description: Event applied }, '200': { description: Event replay }, '409': { description: Event conflict } }
/v1/combinations:
post:
summary: Create a fixed instruction-plus-production combination
description: Creates an organization-scoped durable combination. Set reserve=true for the production-capable path; the operation is idempotent by idempotencyKey.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CombinationRequest' } } } }
responses: { '201': { description: Combination }, '200': { description: Idempotent replay }, '409': { description: Insufficient balance or idempotency conflict } }
/v1/combinations/{id}:
get:
summary: Read a fixed combination
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '200': { description: Combination }, '404': { description: Not found } }
/v1/combinations/{id}/events:
post:
summary: Advance a fixed combination step
description: Instruction must succeed before production; terminal success/failure settles the reserved accounts once.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CombinationEventRequest' } } } }
responses: { '202': { description: Combination updated }, '409': { description: Transition or balance conflict } }
/v1/balance:
get:
summary: Read organization balances
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
responses: { '200': { description: Balance, content: { application/json: { schema: { $ref: '#/components/schemas/Balance' } } } } }
/v1/billing/ledger:
get:
summary: Read the organization ledger
description: Organization-scoped readback of immutable balance operations; no customer write is exposed.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { name: limit, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500 } }]
responses: { '200': { description: Ledger entries } }
/v1/billing/top-ups:
post:
summary: Record an approved offline top-up
description: Admin/owner session only. Requires approval and operator references; operationId is idempotent.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/TopUpRequest' } } } }
responses: { '201': { description: Recorded }, '200': { description: Idempotent replay } }
/v1/billing/exchange:
post:
summary: Exchange fiat to Credits
description: Admin/owner session only. One-way exchange in one database transaction with an approved rate snapshot.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ExchangeRequest' } } } }
responses: { '201': { description: Exchanged }, '409': { description: Insufficient balance } }
/v1/billing/refunds:
post:
summary: Refund to the original account
description: Admin/owner session only. Refunds are bounded by the original settle/debit operation and are idempotent.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RefundRequest' } } } }
responses: { '201': { description: Refunded }, '409': { description: Refund exceeds original operation } }
/v1/billing/policy:
put:
summary: Set reviewed balance policy
description: Admin/owner session only. Defaults remain zero and post-pay is rejected until separately approved.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/BalancePolicyRequest' } } } }
responses: { '200': { description: Updated balance policy } }
/v1/assets:
post:
summary: Register an upstream file for controlled copying
description: Registration remains pending until the object-store copy validates the file. Source URLs reject credentials and private hosts.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/AssetRequest' } } }
responses: { '201': { description: Pending asset }, '400': { description: Invalid source URL } }
/v1/assets/{id}:
get:
summary: Read asset lifecycle
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '200': { description: Asset }, '404': { description: Not found } }
/v1/api-keys:
get:
summary: List organization API keys
description: Admin/owner session only. Secret values are never returned after creation.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
responses: { '200': { description: API keys } }
post:
summary: Create an organization API key
description: Admin/owner session only. Store the returned secret once; the server stores only a digest.
parameters: [{ $ref: '#/components/parameters/OrganizationId' }]
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/APIKeyRequest' } } }
responses: { '201': { description: Created key and one-time secret } }
/v1/api-keys/{id}:
delete:
summary: Revoke an organization API key
parameters: [{ $ref: '#/components/parameters/OrganizationId' }, { $ref: '#/components/parameters/Id' }]
responses: { '204': { description: Revoked }, '404': { description: Not found } }
components:
securitySchemes:
sessionCookie: { type: apiKey, in: cookie, name: better-auth.session_token }
parameters:
OrganizationId: { name: organizationId, in: query, required: true, schema: { type: string } }
Id: { name: id, in: path, required: true, schema: { type: string } }
WebhookSignature: { name: X-UNICUS-Signature, in: header, required: false, description: Required when UNICUS_WEBHOOK_SECRET is configured; HMAC over timestamp and raw body., schema: { type: string } }
schemas:
TaskRequest:
type: object
required: [type, payload, idempotencyKey]
properties:
type: { type: string }
payload: { type: object, additionalProperties: true }
idempotencyKey: { type: string, minLength: 1 }
callbackUrl: { type: string, format: uri, description: Optional customer callback target; production deployments must use HTTPS. }
Task: { type: object, required: [id, organizationId, status, upstreamState], properties: { id: { type: string }, organizationId: { type: string }, status: { type: string }, upstreamState: { type: string }, payload: { type: object }, callbackUrl: { type: string, format: uri } } }
TaskAttempt: { type: object, required: [id, organizationId, taskId, attempt, status], properties: { id: { type: string }, organizationId: { type: string }, taskId: { type: string }, attempt: { type: integer, minimum: 1 }, status: { type: string, enum: [running, succeeded, failed] }, error: { type: object }, startedAt: { type: string, format: date-time }, finishedAt: { type: string, format: date-time } } }
EventRequest: { type: object, required: [eventId, fingerprint, status], properties: { eventId: { type: string }, fingerprint: { type: string }, status: { type: string, enum: [queued, running, succeeded, failed] }, upstreamState: { type: string }, result: { type: object }, error: { type: object } } }
QuoteRequest:
type: object
required: [source, amount, currency, productionReady]
properties:
source: { type: object, additionalProperties: true }
amount: { type: integer, minimum: 0 }
currency: { type: string }
shippingIncluded: { type: boolean }
productionReady: { type: boolean }
validUntil: { type: string, format: date-time, description: Required and must be in the future when productionReady is true; optional for reference estimates. }
OrderRequest: { type: object, required: [quoteId], properties: { quoteId: { type: string }, idempotencyKey: { type: string }, callbackUrl: { type: string, format: uri, description: Optional customer callback target; production deployments must use HTTPS. } } }
OrderEventRequest: { type: object, required: [eventId, fingerprint, status], properties: { eventId: { type: string }, fingerprint: { type: string }, status: { type: string, enum: [repairing, in-production, shipped, delivered, failed] }, upstreamOrderId: { type: string }, tracking: { type: object, additionalProperties: true } } }
CombinationRequest: { type: object, required: [idempotencyKey, credits, fiat], properties: { idempotencyKey: { type: string }, credits: { type: integer, minimum: 0 }, fiat: { type: integer, minimum: 0 }, reserve: { type: boolean, description: Required for the production-capable path } } }
CombinationEventRequest: { type: object, required: [step, status], properties: { step: { type: string, enum: [instruction, production] }, status: { type: string, enum: [pending, succeeded, failed] } } }
Balance: { type: object, properties: { organizationId: { type: string }, creditsAvailable: { type: integer }, fiatAvailable: { type: integer }, creditsReserved: { type: integer }, fiatReserved: { type: integer }, minimumCredits: { type: integer }, minimumFiat: { type: integer }, creditLimitCredits: { type: integer }, creditLimitFiat: { type: integer }, postpayEnabled: { type: boolean }, productionEnabled: { type: boolean } } }
AssetRequest: { type: object, required: [sourceUrl], properties: { sourceUrl: { type: string, format: uri }, taskId: { type: string }, expiresAt: { type: string, format: date-time } } }
APIKeyRequest: { type: object, required: [name], properties: { name: { type: string, minLength: 1, maxLength: 120 } } }
TopUpRequest: { type: object, required: [account, amount, currency, operationId, approvalReference, approvedBy], properties: { account: { type: string, enum: [fiat, credits] }, amount: { type: integer, minimum: 1 }, currency: { type: string }, operationId: { type: string }, approvalReference: { type: string }, approvedBy: { type: string }, receiptReference: { type: string } } }
ExchangeRequest: { type: object, required: [fiatAmount, rateNumerator, rateDenominator, operationId, approvalReference, approvedBy, currency], properties: { fiatAmount: { type: integer, minimum: 1 }, rateNumerator: { type: integer, minimum: 1 }, rateDenominator: { type: integer, minimum: 1 }, operationId: { type: string }, approvalReference: { type: string }, approvedBy: { type: string }, currency: { type: string } } }
RefundRequest: { type: object, required: [sourceOperationId, refundOperationId, amount, reason], properties: { sourceOperationId: { type: string }, refundOperationId: { type: string }, amount: { type: integer, minimum: 1 }, reason: { type: string } } }
BalancePolicyRequest: { type: object, required: [approvalReference], properties: { minimumCredits: { type: integer, minimum: 0 }, minimumFiat: { type: integer, minimum: 0 }, creditLimitCredits: { type: integer, minimum: 0 }, creditLimitFiat: { type: integer, minimum: 0 }, postpayEnabled: { type: boolean }, approvalReference: { type: string } } }