openapi: 3.0.3
info:
  title: Pixelmade Public API
  version: "1.0.0"
  description: >
    Programmatic access to Pixelmade — the print-on-demand supplier for custom
    diamond-painting and craft kits. Register kit designs as templates, look up
    products and DMC colors, place orders, and receive webhook notifications —
    from your own tools or an AI coding agent (Claude Code, ChatGPT, Codex).


    **Authentication.** Every request needs a bearer token, scoped to your brand.
    Create and rotate tokens yourself in the brand portal under **Developer**
    (an admin grants API access first). Send it as `Authorization: Bearer <token>`.


    **Rate limit.** 60 requests/minute per user by default; `429` with a
    `Retry-After` header when exceeded.


    **Orders and payment.** For standard accounts, `POST /orders` creates a payable
    order in your **brand portal** (`pending_payment`) — paying it there is what
    sends it to production. The API never charges you or bypasses payment. Accounts
    on invoice terms submit straight to production and are billed by invoice.
  contact:
    name: Pixelmade
    url: https://www.pixelmade.ai

servers:
  - url: https://app.pixelmade.ai/public_api
    description: Production
  - url: https://staging.pixelmade.ai/public_api
    description: Staging

security:
  - bearerAuth: []

tags:
  - name: Products
    description: The catalog of kit products you can order and design against.
  - name: DMC Colors
    description: The supported DMC color palette, for cross-referencing artwork.
  - name: Product Templates
    description: Register a pre-designed kit so a storefront variant becomes orderable.
  - name: Orders
    description: Place and manage orders. Standard accounts pay in the brand portal.
  - name: Webhooks
    description: Get notified when order events happen (e.g. shipped).

paths:
  /products:
    get:
      tags: [Products]
      summary: List products
      operationId: listProducts
      responses:
        "200":
          description: The active catalog products.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Product" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /products/{id}:
    get:
      tags: [Products]
      summary: Get a product
      operationId: getProduct
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, example: 1 }
      responses:
        "200":
          description: The product.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Product" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /dmc_colors:
    get:
      tags: [DMC Colors]
      summary: List DMC colors
      operationId: listDmcColors
      responses:
        "200":
          description: The full supported DMC palette.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/DmcColor" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /product_templates:
    get:
      tags: [Product Templates]
      summary: List / look up templates
      operationId: listProductTemplates
      parameters:
        - name: slug
          in: query
          required: false
          schema: { type: string }
          description: Filter to a single template by its slug (for idempotent lookups).
      responses:
        "200":
          description: Your brand's templates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  product_templates:
                    type: array
                    items: { $ref: "#/components/schemas/ProductTemplate" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Product Templates]
      summary: Register or update a template (upsert by slug)
      operationId: createProductTemplate
      description: >
        Registers a pre-designed kit and returns the `pmt_` SKU that makes a
        storefront variant orderable. Re-posting the same `slug` updates it in
        place and keeps the SKU stable. The uploaded `dithered_image` is the
        production print file (1 pixel per drill cell, PNG or BMP).
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [name, slug, catalog_product_id, dithered_image]
              properties:
                name: { type: string, example: "Cat Diamond Art Kit — Tabby Portrait" }
                slug:
                  type: string
                  pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
                  example: "cat-diamond-art-kit-tabby-portrait"
                catalog_product_id: { type: integer, example: 3 }
                dithered_image:
                  type: string
                  format: binary
                  description: Production print file — image/png or image/bmp, ≤ 10 MB.
                presentation_image:
                  type: string
                  format: binary
                  description: Hero/mockup image (png/jpeg/webp), ≤ 5 MB.
                original_image:
                  type: string
                  format: binary
                  description: Pre-dither source image (png/jpeg/webp), ≤ 5 MB.
                active: { type: boolean, default: true }
      responses:
        "201":
          description: Template created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProductTemplate" }
        "200":
          description: Existing template (matched by slug) updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProductTemplate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /orders:
    post:
      tags: [Orders]
      summary: Create an order
      operationId: createOrder
      description: >
        Standard accounts: the order lands in your **brand portal's Orders
        section** as `pending_payment` with a `portal_url` — paying it there sends
        it to production. Invoice-term accounts submit straight to production.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OrderRequest" }
      responses:
        "201":
          description: Order created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    # (see estimate_shipping below)
  /orders/estimate_shipping:
    post:
      tags: [Orders]
      summary: Estimate production time + shipping
      operationId: estimateShipping
      description: Quote production/shipping days for a destination and set of line quantities.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_info:
                  type: object
                  properties:
                    country: { type: string, example: "USA" }
                lines:
                  type: array
                  items:
                    type: object
                    properties:
                      quantity: { type: integer, example: 2 }
      responses:
        "200":
          description: The estimate.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ShippingEstimate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /orders/{id}:
    get:
      tags: [Orders]
      summary: Get an order
      operationId: getOrder
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, example: 100 }
      responses:
        "200":
          description: The order.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Orders]
      summary: Cancel an order
      operationId: cancelOrder
      description: >
        Cancellation is only possible up to a point — check `allow_cancel`. If the
        order is already in production, `cancel_type` is `production_cost`; if too
        far along, returns `422`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, example: 100 }
      responses:
        "200":
          description: The updated (canceled) order.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Too far along to cancel. }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhooks
      operationId: listWebhooks
      responses:
        "200":
          description: Your registered webhooks.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Webhooks]
      summary: Register a webhook
      operationId: createWebhook
      description: Register up to 5 webhooks per brand. We POST order events to your address.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [address, event]
              properties:
                address: { type: string, format: uri, example: "https://yourapp.com/webhooks/pixelmade" }
                event: { type: string, example: "order_shipped" }
                secret: { type: string, example: "your_secret" }
      responses:
        "201":
          description: Webhook registered.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /webhooks/{id}:
    get:
      tags: [Webhooks]
      summary: Get a webhook
      operationId: getWebhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: The webhook.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      operationId: deleteWebhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A brand-scoped token from the portal's Developer tab.
  responses:
    Unauthorized:
      description: Missing, invalid, or revoked token.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { errors: [{ status: 401, title: "Not authorized" }] }
    BadRequest:
      description: Validation failed; `detail` lists each problem.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: No such resource for your brand.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Product:
      type: object
      properties:
        id: { type: integer, example: 1 }
        name: { type: string, example: "30x40cm Round Diamond Painting Kit" }
        category_name: { type: string, example: "Diamond Painting" }
        style_name: { type: string, example: "Round" }
        size_name: { type: string, example: "30x40cm" }
    DmcColor:
      type: object
      properties:
        dmc_code: { type: string, example: "310" }
        hex: { type: string, description: "Hex value, no leading #", example: "2c3232" }
    ProductTemplate:
      type: object
      properties:
        id: { type: integer }
        uid: { type: string }
        external_sku: { type: string, description: "pmt_<uid> — put this on your storefront variant.", example: "pmt_a1b2c3" }
        slug: { type: string }
        name: { type: string }
        active: { type: boolean }
        catalog_product_id: { type: integer }
        chart_sha256: { type: string, description: "SHA-256 of the uploaded print file." }
        dithered_image_attached: { type: boolean }
    OrderRequest:
      type: object
      required: [reference_number, customer_info, lines]
      properties:
        reference_number: { type: string, description: "Your internal id; unique per brand.", example: "my_order_001" }
        customer_info: { $ref: "#/components/schemas/CustomerInfo" }
        lines:
          type: array
          minItems: 1
          items:
            type: object
            required: [product_id, quantity, artwork_location]
            properties:
              product_id: { type: integer, example: 1 }
              quantity: { type: integer, minimum: 1, example: 1 }
              artwork_location: { type: string, format: uri, description: "HTTPS URL to the artwork.", example: "https://example.com/art.bmp" }
    Order:
      type: object
      properties:
        id: { type: integer, example: 100 }
        reference_number: { type: string, example: "my_order_001" }
        state:
          type: string
          description: "`pending_payment` until paid in the portal (standard accounts); then production → shipped → delivered."
          example: "pending_payment"
        payment_status:
          type: string
          enum: [pending_payment, paid]
          description: "Present for portal (payable) orders."
        portal_url:
          type: string
          description: "Where to pay a portal order. Present for payable orders."
          example: "https://app.pixelmade.ai/spa/orders"
        created_at: { type: string, format: date-time }
        customer_info: { $ref: "#/components/schemas/CustomerInfo" }
        lines:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              product_id: { type: integer }
              quantity: { type: integer }
              artwork_location: { type: string }
        tracking_info:
          type: array
          description: Empty until shipped.
          items:
            type: object
            properties:
              carrier: { type: string, example: "DHL" }
              tracking_number: { type: string }
              tracking_url: { type: string }
        allow_cancel: { type: boolean }
        cancel_type: { type: string, nullable: true, enum: [no_cost, production_cost, null] }
    CustomerInfo:
      type: object
      required: [name, address1, country]
      properties:
        name: { type: string, example: "Jane Smith" }
        email: { type: string, example: "jane@example.com" }
        phone: { type: string, example: "555-1234" }
        address1: { type: string, example: "123 Main St" }
        address2: { type: string, example: "Apt 4" }
        city: { type: string, example: "Seattle" }
        state: { type: string, example: "WA" }
        postal_code: { type: string, example: "98001" }
        country: { type: string, description: "2- or 3-letter ISO 3166 code.", example: "USA" }
    ShippingEstimate:
      type: object
      properties:
        total_pieces: { type: integer, example: 2 }
        estimated_production_days_min: { type: integer, example: 7 }
        estimated_production_days_max: { type: integer, example: 10 }
        shipping_info:
          type: array
          items:
            type: object
            properties:
              name: { type: string, example: "Economy" }
              estimated_shipping_days_min: { type: integer, example: 12 }
              estimated_shipping_days_max: { type: integer, example: 16 }
    Webhook:
      type: object
      properties:
        id: { type: integer }
        address: { type: string, format: uri }
        event: { type: string, example: "order_shipped" }
    Error:
      type: object
      properties:
        errors:
          type: array
          items:
            type: object
            properties:
              status: { type: integer, example: 400 }
              title: { type: string }
              detail: { type: string }
