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 } } }