openapi: 3.1.0
info:
  title: CathyColor API
  version: 1.0.0-beta
  description: >-
    Production server-to-server API for matching RGB values and searching
    names across 161 Chinese colors, with endpoints for library and usage
    metadata. It is not a full-catalog download, and direct browser calls are
    not supported (no CORS). Production host:
    https://api.chinesecoloratlas.com. New API sales and provisioning for new
    customer accounts are paused. Existing customers can continue using the API
    with active keys; renewals and support continue. Existing account limits
    reset at the start of each UTC calendar month: 2,000 match input colors and
    1,000 searches. The first quota period may be partial. Quotas do not roll
    over; there are no automatic top-ups or overage charges. No SLA is offered.
    All metered operations require an Idempotency-Key.
servers:
  - url: https://api.chinesecoloratlas.com
    description: Production host for existing customer accounts; requests require an active API key.
paths:
  /v1/library:
    get:
      summary: Get cleared-release dataset metadata
      operationId: getLibrary
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Dataset metadata only; never a color-record array.
          headers: { X-Request-Id: { $ref: '#/components/headers/RequestId' } }
          content: { application/json: { schema: { $ref: '#/components/schemas/Library' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Unavailable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
  /v1/usage:
    get:
      summary: Get current monthly usage
      operationId: getUsage
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: UTC-month usage for both commercial meters.
          headers: { X-Request-Id: { $ref: '#/components/headers/RequestId' } }
          content: { application/json: { schema: { $ref: '#/components/schemas/UsageResponse' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Unavailable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
  /v1/match:
    post:
      summary: Match one to five RGB colors against the cleared-release projection
      operationId: matchColors
      security: [{ bearerAuth: [] }]
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/MatchRequest' } } }
      responses:
        '200':
          description: One to three matches for each input. A replay has Idempotency-Replayed true.
          headers:
            X-Request-Id: { $ref: '#/components/headers/RequestId' }
            X-Usage-Period: { $ref: '#/components/headers/UsagePeriod' }
            X-Usage-Meter: { $ref: '#/components/headers/UsageMeter' }
            X-Usage-Limit: { $ref: '#/components/headers/UsageValue' }
            X-Usage-Committed: { $ref: '#/components/headers/UsageValue' }
            X-Usage-Reserved: { $ref: '#/components/headers/UsageValue' }
            X-Usage-Remaining: { $ref: '#/components/headers/UsageValue' }
            Idempotency-Replayed: { schema: { type: string, enum: ['true'] } }
          content: { application/json: { schema: { $ref: '#/components/schemas/MatchResponse' } } }
        '400': { $ref: '#/components/responses/IdempotencyRequired' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Unavailable' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '415': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/MeteredTooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/CommitUnavailable' }
  /v1/search:
    get:
      summary: Search the cleared-release projection
      operationId: searchColors
      security: [{ bearerAuth: [] }]
      parameters:
        - { $ref: '#/components/parameters/IdempotencyKey' }
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2, maxLength: 64 }
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 10, default: 10 }
      responses:
        '200':
          description: At most ten minimal color records; no full-library listing is available.
          headers:
            X-Request-Id: { $ref: '#/components/headers/RequestId' }
            X-Usage-Period: { $ref: '#/components/headers/UsagePeriod' }
            X-Usage-Meter: { $ref: '#/components/headers/UsageMeter' }
            X-Usage-Limit: { $ref: '#/components/headers/UsageValue' }
            X-Usage-Committed: { $ref: '#/components/headers/UsageValue' }
            X-Usage-Reserved: { $ref: '#/components/headers/UsageValue' }
            X-Usage-Remaining: { $ref: '#/components/headers/UsageValue' }
            Idempotency-Replayed: { schema: { type: string, enum: ['true'] } }
          content: { application/json: { schema: { $ref: '#/components/schemas/SearchResponse' } } }
        '400': { $ref: '#/components/responses/IdempotencyRequired' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Unavailable' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
        '422': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/MeteredTooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/CommitUnavailable' }
components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: CathyColor API key }
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 8, maxLength: 128, pattern: '^[A-Za-z0-9._:-]+$' }
  headers:
    RequestId: { schema: { type: string, format: uuid } }
    UsagePeriod: { schema: { type: string, pattern: '^\\d{4}-\\d{2}$' } }
    UsageMeter: { schema: { type: string, enum: [color_match, color_search] } }
    UsageValue: { schema: { type: string, pattern: '^\\d+$' } }
  responses:
    Error:
      description: Request failed.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: Missing, malformed, revoked, or invalid API key.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unavailable:
      description: Account is suspended or unavailable.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    IdempotencyRequired:
      description: A metered request omitted a valid Idempotency-Key; match also returns body_required or invalid_json for a missing or malformed JSON body.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    IdempotencyConflict:
      description: >-
        Either idempotency_conflict (the key was used with different input), or
        request_in_progress (the original request is still running). For
        request_in_progress, wait for Retry-After and retry the same key and input;
        do not create another key for a retry of the same logical operation.
      headers: { Retry-After: { description: Present for request_in_progress only., schema: { type: string } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    PayloadTooLarge:
      description: The match body exceeds the server byte limit; error is payload_too_large.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    InternalError:
      description: Internal failure; retry a metered operation with the same Idempotency-Key and input to avoid duplicate charges.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    CommitUnavailable:
      description: Usage commit temporarily unavailable; error is usage_commit_failed. Retry the same Idempotency-Key and input.
      headers: { Retry-After: { schema: { type: string } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Abuse rate limit reached; error is rate_limited.
      headers: { Retry-After: { schema: { type: string } } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    MeteredTooManyRequests:
      description: >-
        Either abuse limiting (error rate_limited, Retry-After 60 seconds) or
        commercial quota exhaustion (error quota_exceeded, Retry-After 86400 seconds).
        Only quota_exceeded includes the X-Usage-* headers below; rate_limited is
        rejected before metering and does not include usage headers. Retry-After
        is a retry hint, not a promise that a monthly quota will have reset.
      headers:
        Retry-After: { schema: { type: string } }
        X-Usage-Period: { $ref: '#/components/headers/UsagePeriod' }
        X-Usage-Meter: { $ref: '#/components/headers/UsageMeter' }
        X-Usage-Limit: { $ref: '#/components/headers/UsageValue' }
        X-Usage-Committed: { $ref: '#/components/headers/UsageValue' }
        X-Usage-Reserved: { $ref: '#/components/headers/UsageValue' }
        X-Usage-Remaining: { $ref: '#/components/headers/UsageValue' }
      content:
        application/json:
          schema:
            allOf:
              - { $ref: '#/components/schemas/Error' }
              - type: object
                properties:
                  error: { type: string, enum: [rate_limited, quota_exceeded] }
          examples:
            rateLimited:
              value: { error: rate_limited, requestId: '00000000-0000-4000-8000-000000000001' }
            quotaExceeded:
              value: { error: quota_exceeded, requestId: '00000000-0000-4000-8000-000000000002' }
  schemas:
    RGB:
      type: object
      required: [r, g, b]
      properties:
        r: { type: number, minimum: 0, maximum: 255 }
        g: { type: number, minimum: 0, maximum: 255 }
        b: { type: number, minimum: 0, maximum: 255 }
    Color:
      type: object
      required: [slug, name, hex, reliability]
      properties:
        slug: { type: string }
        name:
          type: object
          required: [zh, en, pinyin]
          properties: { zh: { type: string }, en: { type: string }, pinyin: { type: string } }
        hex: { type: string, pattern: '^#[0-9A-F]{6}$' }
        reliability: { type: string, enum: [verified, attributed, disputed] }
    Library:
      type: object
      required: [version, recordCount, sourceProjectionVersion, sourceProjectionSha256, rightsManifestSha256, librarySha256, mode]
      properties:
        version: { type: string }
        recordCount: { type: integer, minimum: 1 }
        valueMismatchCount: { type: integer, minimum: 0 }
        sourceProjectionVersion: { type: string }
        sourceProjectionSha256: { type: string, pattern: '^[a-f0-9]{64}$' }
        rightsManifestSha256: { type: string, pattern: '^[a-f0-9]{64}$' }
        librarySha256: { type: string, pattern: '^[a-f0-9]{64}$' }
        mode: { type: string }
    UsageSnapshot:
      type: object
      required: [period, meter, limit, committed, reserved, remaining]
      properties:
        period: { type: string }
        meter: { type: string, enum: [color_match, color_search] }
        limit: { type: integer, minimum: 0 }
        committed: { type: integer, minimum: 0 }
        reserved: { type: integer, minimum: 0 }
        remaining: { type: integer, minimum: 0 }
    UsageResponse:
      type: object
      required: [outcome, accountId, planId, status, usage]
      properties:
        outcome: { type: string, enum: [ok] }
        accountId: { type: string }
        planId: { type: string }
        status: { type: string, enum: [active] }
        usage: { type: array, minItems: 2, maxItems: 2, items: { $ref: '#/components/schemas/UsageSnapshot' } }
    MatchRequest:
      type: object
      required: [colors]
      properties:
        colors: { type: array, minItems: 1, maxItems: 5, items: { $ref: '#/components/schemas/RGB' } }
        top: { type: integer, minimum: 1, maximum: 3, default: 1 }
    MatchResult:
      type: object
      required: [input, matches]
      properties:
        input: { $ref: '#/components/schemas/RGB' }
        matches:
          type: array
          minItems: 1
          maxItems: 3
          items:
            allOf:
              - { $ref: '#/components/schemas/Color' }
              - type: object
                required: [deltaE, similarity]
                properties: { deltaE: { type: number }, similarity: { type: number } }
    MatchResponse:
      type: object
      required: [datasetVersion, results]
      properties:
        datasetVersion: { type: string }
        results: { type: array, minItems: 1, maxItems: 5, items: { $ref: '#/components/schemas/MatchResult' } }
    SearchResponse:
      type: object
      required: [datasetVersion, count, results]
      properties:
        datasetVersion: { type: string }
        count: { type: integer, minimum: 0, maximum: 10 }
        results: { type: array, maxItems: 10, items: { $ref: '#/components/schemas/Color' } }
    Error:
      type: object
      required: [error, requestId]
      properties:
        error: { type: string }
        requestId: { type: string, format: uuid }
