openapi: 3.1.0
info:
  title: OnMe API
  version: "1.0"
  description: |
    Virtual try-on for online stores. Public keys (pk_live_, pk_test_) are used in the browser and only work from the
    site's verified domain. Secret keys (sk_live_, sk_test_) stay on your server. Test keys return a sample result
    at no cost and work on localhost.
servers:
  - url: https://api.onme.example/v1
security:
  - bearer: []
components:
  securitySchemes:
    bearer: { type: http, scheme: bearer, description: "pk_* or sk_* key" }
  schemas:
    Error:
      type: object
      properties:
        error_code:
          type: string
          enum: [invalid_image, rejected_framing, rejected_moderation, rejected_age, origin_not_allowed, site_not_verified, product_not_enabled,
                 quota_exceeded, rate_limited, session_limit, busy, capacity, consent_required, bad_request, invalid_key, missing_key, not_found, internal]
        message: { type: string }
        hint: { type: string, description: Text you can show to the shopper }
    TryOn:
      type: object
      properties:
        id: { type: string }
        status: { type: string, enum: [queued, running, succeeded, failed] }
        token: { type: string, description: Send as X-Job-Token when polling }
        position: { type: integer, description: Jobs ahead in the queue }
        eta_seconds: { type: integer }
        result_url: { type: string, format: uri, description: Private Worker image link valid about 45 seconds, never publicly cached }
        low_confidence: { type: boolean }
        error_code: { type: string }
        hint: { type: string }
    Product:
      type: object
      required: [id, image_url, category]
      properties:
        id: { type: string, description: Your SKU }
        title: { type: string }
        url: { type: string, format: uri }
        image_url: { type: string, format: uri }
        category: { type: string, description: "Any supported label, English or Bangla (e.g. saree, শাড়ি, jamdani, glasses). See GET /categories." }
        price: { type: number }
        currency: { type: string }
        enabled: { type: boolean }
  responses:
    Error:
      description: Error
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
paths:
  /categories:
    get:
      summary: Supported product types
      security: []
      responses:
        "200": { description: OK }
  /uploads:
    post:
      summary: Get a signed URL to upload the shopper's photo (public key)
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { content_type: { type: string, enum: [image/jpeg, image/png, image/webp, image/heic] }, size: { type: integer } } }
      responses:
        "200":
          description: Upload with PUT to upload_url within 5 minutes
          content: { application/json: { schema: { type: object, properties: { upload_url: { type: string }, upload_key: { type: string }, headers: { type: object } } } } }
        "4XX": { $ref: "#/components/responses/Error" }
  /tryons:
    post:
      summary: Start a try-on (public key)
      parameters:
        - { name: Idempotency-Key, in: header, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [product_id, upload_key, session_id, consent]
              properties:
                product_id: { type: string }
                upload_key: { type: string }
                session_id: { type: string }
                consent: { type: boolean, const: true }
                product_image: { type: string, format: uri, description: Only needed if the product is not synced }
                category: { type: string, description: Only needed if the product is not synced }
      responses:
        "201": { description: Queued, content: { application/json: { schema: { $ref: "#/components/schemas/TryOn" } } } }
        "503": { description: "busy: retry after the Retry-After header", content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "4XX": { $ref: "#/components/responses/Error" }
  /tryons/{id}:
    get:
      summary: Poll a try-on (public key). Poll every 1.5-2.5 s.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: X-Job-Token, in: header, required: true, schema: { type: string } }
      responses:
        "200": { description: Status, content: { application/json: { schema: { $ref: "#/components/schemas/TryOn" } } } }
        "404": { $ref: "#/components/responses/Error" }
  /tryons/{id}/displayed:
    post:
      summary: Acknowledge a successfully displayed try-on (public key; idempotent, free for cached repeats)
      description: |
        Call only after the private result image has successfully loaded. Polling never consumes included
        usage. The server accounts a non-cached result at most once. Keep the job token out of URL parameters.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: X-Job-Token, in: header, required: true, schema: { type: string } }
      responses:
        "200": { description: Acknowledgement accepted; duplicate acknowledgements are free }
        "410": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
  /private-results/{id}:
    get:
      summary: Short-lived private result image; only use the link returned by a valid poll
      security: []
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: exp, in: query, required: true, schema: { type: integer } }
        - { name: sig, in: query, required: true, schema: { type: string } }
      responses:
        "200": { description: Private image/webp; no-store; not eligible for CDN caching }
        "403": { $ref: "#/components/responses/Error" }
        "410": { $ref: "#/components/responses/Error" }
  /events:
    post:
      summary: Widget analytics, up to 50 per call (public key)
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { events: { type: array, items: { type: object, properties: { name: { type: string }, session_id: { type: string }, product_id: { type: string }, props: { type: object } } } } } }
      responses:
        "200": { description: Accepted }
  /products/resolve:
    get:
      summary: Whether a product can be tried on (public key)
      parameters:
        - { name: external_id, in: query, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }
  /widget-config:
    get:
      summary: The site's widget settings from the dashboard (public key)
      responses:
        "200": { description: OK }
  /catalog/sync:
    post:
      summary: Add or update up to 500 products (secret key)
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, properties: { products: { type: array, maxItems: 500, items: { $ref: "#/components/schemas/Product" } } } }
      responses:
        "202": { description: Accepted; images are prepared in the background }
        "4XX": { $ref: "#/components/responses/Error" }
  /orders:
    post:
      summary: Report an order for try-on attribution (secret key)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [order_id]
              properties:
                order_id: { type: string }
                session_id: { type: string, description: "From window.OnMe.getSessionId() at checkout" }
                session_ids: { type: array, items: { type: string } }
                value: { type: number }
                currency: { type: string }
      responses:
        "200": { description: OK }
