openapi: 3.0.3

info:
  title: Bolder API v1
  version: "1.0"
  description: |
    The Bolder API is a hypermedia-driven REST API following the [HAL](https://stateless.group/hal_specification.html)
    (Hypertext Application Language) specification.

    ## HAL conventions

    Every response includes a `_links` object with related resources and actions. Links use the `btc:` CURIE
    namespace — for example `btc:products` expands to `https://api.onbolder.com/docs/v1#products`.

    Non-GET actions (POST, PUT, DELETE) are described by links that carry a `method` property.

    Templated links (e.g. pagination, filters) carry `"templated": true` and use RFC 6570 URI Templates.

    Embedded sub-resources live in `_embedded` and follow the same HAL structure.

    ## Paginated results

    List endpoints return a paginated results envelope:

    ```json
    {
      "_links": {
        "self": { "href": "…?page=1" },
        "next": { "href": "…?page=2" },
        "last": { "href": "…?page=10" }
      },
      "total_items": 500,
      "per_page": 50,
      "page": 1,
      "_embedded": { "items": [ … ] }
    }
    ```

    ## Authentication

    All requests require a Bearer access token obtained via OAuth 2.0.

    ```
    Authorization: Bearer YOUR-ACCESS-TOKEN
    ```

    See the [Authentication](#tag/Authentication) section for the full list of supported OAuth 2.0 flows.

servers:
  - url: https://api.onbolder.com/v1

security:
  - bearerAuth: []

tags:
  - name: Authentication
    description: |
      OAuth 2.0 access tokens are issued by `https://auth.onbolder.com`. The following flows are supported:

      1. **Authorization Code** — for web apps acting on behalf of a logged-in user.
      2. **Implicit** — for JavaScript / single-page apps.
      3. **Client Credentials** — for server-side scripts and automated jobs.
      4. **Resource Owner Password** — for trusted first-party apps.
      5. **JWT Bearer** — to exchange an expired token for a fresh one.

      All token requests go to `POST https://auth.onbolder.com/oauth/token`.

      ### Scopes

      | Scope    | Description |
      |----------|-------------|
      | `public` | Read-only access to publicly available data |
      | `admin`  | Full read/write access scoped to the authenticated account |

  - name: Root
    description: Entry point that returns the authenticated user's account and linked shops.
  - name: Shops
    description: Shops owned by the authenticated account.
  - name: Products
    description: Products catalogue management.
  - name: Variants
    description: Product variants (size, colour, etc.).
  - name: Product Images
    description: Upload and delete product images.
  - name: Orders
    description: Order management including creation, updates, and batch operations.
  - name: Contacts
    description: Shop contacts (customers).
  - name: Webhooks
    description: Subscribe to real-time event notifications via webhooks.
  - name: Themes
    description: Shop theme templates and static assets.

paths:

  # ── Root ──────────────────────────────────────────────────────────────────
  /:
    get:
      tags: [Root]
      summary: Root endpoint
      description: |
        Returns the authenticated user's account information and an embedded list of their shops,
        each carrying HAL links to all available actions (products, orders, contacts, theme, etc.).
      operationId: getRoot
      responses:
        "200":
          description: Authenticated root resource
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RootResource"
              example:
                _links:
                  curies:
                    - name: btc
                      href: "https://api.onbolder.com/docs/v1#{rel}"
                      templated: true
                  btc:all_products:
                    href: "https://api.onbolder.com/v1/products{?q,page,per_page,status,shop_subdomains,collections,shop_ids,tags,variants,sku,currency,price.gte,price.lte,sort}"
                    templated: true
                    title: All products, with search and filters
                uid: 2
                email: user@site.com
                user_name: johnnie85
                scopes: "admin,public"
                _embedded:
                  shops:
                    - id: 47
                      subdomain: acme
                      url: acme.onbolder.com
                      name: Acme
                      currency_code: CLP

  # ── Shops ─────────────────────────────────────────────────────────────────
  /shops:
    get:
      tags: [Shops]
      summary: List shops
      description: Returns a paginated list of shops accessible to the authenticated account.
      operationId: listShops
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated shops list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShopResults"

  /shops/{id}:
    get:
      tags: [Shops]
      summary: Shop details
      description: |
        Returns full details for a single shop, including HAL links for all available
        actions (products, orders, contacts, theme, variants).
      operationId: getShop
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Shop ID or subdomain
      responses:
        "200":
          description: Shop resource
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShopDetails"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Products ──────────────────────────────────────────────────────────────
  /products:
    get:
      tags: [Products]
      summary: List all products
      description: |
        Returns a paginated list of products across all shops accessible to the token.
        Supports rich filtering, full-text search, and sorting.
      operationId: listProducts
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: q
          in: query
          schema:
            type: string
          description: Full-text search query
        - name: status
          in: query
          schema:
            type: string
            enum: [visible, hidden, all]
            default: visible
          description: Visibility status filter
        - name: shop_subdomains
          in: query
          schema:
            type: string
          description: "Comma-separated shop subdomains, e.g. `romano,acme`"
        - name: shop_ids
          in: query
          schema:
            type: string
          description: "Comma-separated shop IDs, e.g. `123,456`"
        - name: collections
          in: query
          schema:
            type: string
          description: "Comma-separated collection slugs, e.g. `featured,summer`"
        - name: tags
          in: query
          schema:
            type: string
          description: "Comma-separated tag names, e.g. `green,red`"
        - name: variants
          in: query
          schema:
            type: string
          description: "Comma-separated variant titles, e.g. `xl,s`"
        - name: sku
          in: query
          schema:
            type: string
          description: "Comma-separated SKU codes"
        - name: currency
          in: query
          schema:
            type: string
          description: "Currency code, e.g. `USD`"
        - name: price_gte
          in: query
          schema:
            type: integer
          description: Minimum price in cents (inclusive)
        - name: price_lte
          in: query
          schema:
            type: integer
          description: Maximum price in cents (inclusive)
        - name: discount_percentage_gte
          in: query
          schema:
            type: number
          description: Minimum discount percentage (inclusive)
        - name: discount_percentage_lte
          in: query
          schema:
            type: number
          description: Maximum discount percentage (inclusive)
        - name: created_on_gte
          in: query
          schema:
            type: string
            format: date-time
          description: "Created on or after, e.g. `2025-12-22 12:00:00`"
        - name: created_on_lte
          in: query
          schema:
            type: string
            format: date-time
          description: Created on or before
        - name: updated_on_gte
          in: query
          schema:
            type: string
            format: date-time
          description: Updated on or after
        - name: updated_on_lte
          in: query
          schema:
            type: string
            format: date-time
          description: Updated on or before
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - title.asc
              - title.desc
              - price.asc
              - price.desc
              - created_on.asc
              - created_on.desc
              - updated_on.asc
              - updated_on.desc
              - discount_percentage.asc
              - discount_percentage.desc
            default: created_on.desc
          description: Sort column and direction
      responses:
        "200":
          description: Paginated products list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductResults"

  /products/{id}:
    get:
      tags: [Products]
      summary: Product details
      description: Returns full details for a single product including embedded variants, images, collections and custom attributes.
      operationId: getProduct
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Product ID or slug
      responses:
        "200":
          description: Product resource
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductDetails"
        "404":
          $ref: "#/components/responses/NotFound"

    delete:
      tags: [Products]
      summary: Delete product
      description: Permanently deletes a product and all its variants and images. Requires `admin` scope.
      operationId: deleteProduct
      security:
        - bearerAuth: [admin]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Product ID or slug
      responses:
        "204":
          description: Product deleted
        "404":
          $ref: "#/components/responses/NotFound"

  /shops/{shop_id}/products:
    post:
      tags: [Products]
      summary: Create product
      description: Creates a new product in the given shop. Requires `admin` scope.
      operationId: createProduct
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductInput"
            example:
              title: iPhone 5
              price: 20020
              price_comparison: 20122
              description: iPhone 5 by Apple
              status: visible
              product_type_name: Smart phones
              tags: [communications, handheld]
              vendor: Acme
              variants:
                - title: White
                  sku: 1234R
                  available_if_no_stock: 1
                  stock: 102
                  weight_in_grams: 340
              collections:
                - id: 1234
                - title: A new collection
      responses:
        "201":
          description: Product created
          headers:
            X-OAuth-Scopes:
              schema:
                type: string
              example: admin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductDetails"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /shops/{shop_id}/products/{id}:
    put:
      tags: [Products]
      summary: Update product
      description: |
        Updates an existing product. All fields are optional — only supplied fields are updated.
        Requires `admin` scope.
      operationId: updateProduct
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Product ID or slug
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductInput"
      responses:
        "200":
          description: Product updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductDetails"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  # ── Variants ───────────────────────────────────────────────────────────────
  /shops/{shop_id}/variants/{id}:
    get:
      tags: [Variants]
      summary: Variant details
      description: Returns details for a single product variant.
      operationId: getVariant
      parameters:
        - $ref: "#/components/parameters/shop_id"
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Variant ID
      responses:
        "200":
          description: Variant resource
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VariantDetails"
        "404":
          $ref: "#/components/responses/NotFound"

    put:
      tags: [Variants]
      summary: Update variant
      description: |
        Updates an existing variant. You can also look up the variant by `sku` instead of `id`
        by passing `?sku=YOUR_SKU` as a query parameter. Requires `admin` scope.
      operationId: updateVariant
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Variant ID (or any value when using `?sku=` lookup)
        - name: sku
          in: query
          schema:
            type: string
          description: Look up variant by SKU instead of ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VariantInput"
      responses:
        "200":
          description: Variant updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VariantDetails"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

    delete:
      tags: [Variants]
      summary: Delete variant
      description: Removes a variant from its product. Requires `admin` scope.
      operationId: deleteVariant
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Variant ID
      responses:
        "204":
          description: Variant deleted
        "404":
          $ref: "#/components/responses/NotFound"

  /products/{product_id}/variants:
    post:
      tags: [Variants]
      summary: Create variant
      description: Adds a new variant to an existing product. Requires `admin` scope.
      operationId: createVariant
      security:
        - bearerAuth: [admin]
      parameters:
        - name: product_id
          in: path
          required: true
          schema:
            type: string
          description: Product ID or slug
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VariantInput"
            example:
              title: Large
              sku: SKU-LG
              stock: 10
              available_if_no_stock: 0
              weight_in_grams: 200
      responses:
        "201":
          description: Variant created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VariantDetails"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  # ── Product Images ─────────────────────────────────────────────────────────
  /products/{product_id}/assets:
    post:
      tags: [Product Images]
      summary: Upload product image
      description: |
        Uploads a new image to a product. The image data must be Base64-encoded and sent in the
        `data` field along with the file name and MIME type. Requires `admin` scope.
      operationId: createProductAsset
      security:
        - bearerAuth: [admin]
      parameters:
        - name: product_id
          in: path
          required: true
          schema:
            type: string
          description: Product ID or slug
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductAssetInput"
            example:
              data: "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
              file_name: photo.jpg
              title: Front view
      responses:
        "201":
          description: Image uploaded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductAssetDetails"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /products/{product_id}/assets/{id}:
    delete:
      tags: [Product Images]
      summary: Delete product image
      description: Permanently removes an image from a product. Requires `admin` scope.
      operationId: deleteProductAsset
      security:
        - bearerAuth: [admin]
      parameters:
        - name: product_id
          in: path
          required: true
          schema:
            type: string
          description: Product ID or slug
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Asset ID
      responses:
        "204":
          description: Asset deleted
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Orders ────────────────────────────────────────────────────────────────
  /shops/{shop_id}/orders:
    get:
      tags: [Orders]
      summary: List orders
      description: |
        Returns a paginated list of orders for a shop. Requires `admin` scope.
        Supports filtering by status, date ranges, amounts, and geo-location.
      operationId: listOrders
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: q
          in: query
          schema:
            type: string
          description: Full-text search
        - name: status
          in: query
          schema:
            type: string
          description: |
            Comma-separated order statuses. Use `successful` as a shorthand for `closed,shipped`.
            Values: `all`, `checkout`, `pending`, `closed`, `shipped`, `invalid`, `successful`
        - name: code
          in: query
          schema:
            type: string
          description: "Order code, e.g. `R1C3964`"
        - name: product_id
          in: query
          schema:
            type: integer
          description: Filter by product ID
        - name: payment_method
          in: query
          schema:
            type: string
          description: "Payment method slug, e.g. `webpay`"
        - name: created_on_gte
          in: query
          schema:
            type: string
            format: date
          description: Created on or after
        - name: created_on_lte
          in: query
          schema:
            type: string
            format: date
          description: Created on or before
        - name: updated_on_gte
          in: query
          schema:
            type: string
            format: date
        - name: updated_on_lte
          in: query
          schema:
            type: string
            format: date
        - name: pending_on_gte
          in: query
          schema:
            type: string
            format: date
        - name: pending_on_lte
          in: query
          schema:
            type: string
            format: date
        - name: closed_on_gte
          in: query
          schema:
            type: string
            format: date
        - name: closed_on_lte
          in: query
          schema:
            type: string
            format: date
        - name: shipped_on_gte
          in: query
          schema:
            type: string
            format: date
        - name: shipped_on_lte
          in: query
          schema:
            type: string
            format: date
        - name: net_total_gte
          in: query
          schema:
            type: integer
          description: Net total in cents, minimum
        - name: net_total_lte
          in: query
          schema:
            type: integer
          description: Net total in cents, maximum
        - name: total_gte
          in: query
          schema:
            type: integer
        - name: total_lte
          in: query
          schema:
            type: integer
        - name: shipping_total_gte
          in: query
          schema:
            type: integer
        - name: shipping_total_lte
          in: query
          schema:
            type: integer
        - name: discount_total_gte
          in: query
          schema:
            type: integer
        - name: discount_total_lte
          in: query
          schema:
            type: integer
        - name: lat_long
          in: query
          schema:
            type: string
          description: "Latitude,longitude pair for geo-filtering, e.g. `0.54322222,1.4444422`"
        - name: distance
          in: query
          schema:
            type: string
            default: 5km
          description: "Radius from `lat_long`, e.g. `10km`"
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - created_on.asc
              - created_on.desc
              - updated_on.asc
              - updated_on.desc
              - pending_on.asc
              - pending_on.desc
              - closed_on.asc
              - closed_on.desc
              - shipped_on.asc
              - shipped_on.desc
            default: created_on.desc
      responses:
        "200":
          description: Paginated orders list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResults"

    post:
      tags: [Orders]
      summary: Create order
      description: |
        Creates a new order in `checkout` state with the given line items, contact and shipping address.
        Returns the new order including web checkout and cart links. Requires `admin` scope.
      operationId: createOrder
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderInput"
            example:
              line_items:
                - variant_id: 1234
                  units: 2
              contact:
                email: joe@bloggs.com
                name: Joe Bloggs
                phone_number: "12322222222"
              address:
                street: Av. Brasil 20
                street_2: 4to piso
                locality_name: Valparaiso
                region_name: Valparaiso
                country_code: CL
      responses:
        "201":
          description: Order created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderDetails"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /shops/{shop_id}/orders/{code}:
    get:
      tags: [Orders]
      summary: Order details
      description: Returns full details for a single order. Requires `admin` scope.
      operationId: getOrder
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: "Order code (e.g. `R1C3964`) or numeric ID"
      responses:
        "200":
          description: Order resource
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderDetails"
        "404":
          $ref: "#/components/responses/NotFound"

    put:
      tags: [Orders]
      summary: Update order
      description: |
        Updates an order that is in `checkout` state. Use this to set or modify line items,
        contact information, and shipping address. Requires `admin` scope.

        The returned order includes `btc:web_checkout` and `btc:web_cart` links for directing
        customers to complete payment.

        **Line item tips:**
        - Pass `units: -1` to remove a line item
        - Contact is matched by `email`; new contacts require `name` and `phone_number` too
      operationId: updateOrder
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: Order code or ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderUpdateInput"
      responses:
        "200":
          description: Order updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderDetails"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /shops/{shop_id}/orders/updaters:
    post:
      tags: [Orders]
      summary: Batch update orders
      description: |
        Updates one or more order attributes across multiple orders at once. Currently supports
        updating `status`. The operation is not transactional — orders that fail validation are
        reported in `invalid_items` while successfully updated ones appear in `valid_items`.
        Requires `admin` scope.
      operationId: batchUpdateOrders
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids, additions]
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                  description: Array of order IDs to update
                  example: [577048, 12001]
                additions:
                  type: object
                  description: Attributes to apply to all orders
                  properties:
                    status:
                      type: string
                      enum: [pending, closed, shipped, invalid]
                      description: New status to set
            example:
              ids: [577048, 12001]
              additions:
                status: shipped
      responses:
        "200":
          description: Batch update result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderBatchResult"

  # ── Contacts ───────────────────────────────────────────────────────────────
  /shops/{shop_id}/contacts:
    get:
      tags: [Contacts]
      summary: List shop contacts
      description: Returns a paginated list of contacts (customers) for a shop. Requires `admin` scope.
      operationId: listContacts
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: q
          in: query
          schema:
            type: string
          description: Full-text search (name, email)
        - name: created_on_gte
          in: query
          schema:
            type: string
            format: date
        - name: created_on_lte
          in: query
          schema:
            type: string
            format: date
        - name: updated_on_gte
          in: query
          schema:
            type: string
            format: date
        - name: updated_on_lte
          in: query
          schema:
            type: string
            format: date
        - name: sort
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated contacts list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactResults"

  # ── Webhooks ───────────────────────────────────────────────────────────────
  /hub/{shop_id}:
    get:
      tags: [Webhooks]
      summary: Webhooks hub
      description: |
        Returns the webhooks hub for a shop, including HAL links to subscribe, unsubscribe and
        list subscriptions.
      operationId: getHub
      parameters:
        - name: shop_id
          in: path
          required: true
          schema:
            type: integer
          description: Shop ID
      responses:
        "200":
          description: Hub resource
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HubResource"

  /hub/{shop_id}/subscriptions:
    get:
      tags: [Webhooks]
      summary: List subscriptions
      description: Returns a paginated list of webhook subscriptions for a shop.
      operationId: listSubscriptions
      parameters:
        - name: shop_id
          in: path
          required: true
          schema:
            type: integer
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, active, failed_activation, failed, disabled]
      responses:
        "200":
          description: Paginated subscriptions list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubscriptionResults"

    post:
      tags: [Webhooks]
      summary: Subscribe
      description: |
        Creates a new webhook subscription. After creation the API sends a verification POST to
        your `url` containing a `X-Hook-Ping` header. Your endpoint must respond with a matching
        `X-Hook-Pong` header to activate the subscription.

        ### Subscription states

        | Status | Description |
        |--------|-------------|
        | `pending` | Created, awaiting verification |
        | `active` | Verified and receiving events |
        | `failed_activation` | Verification was rejected |
        | `failed` | Disabled after 3 consecutive delivery failures |
        | `disabled` | Manually disabled or target returned `410` |

        ### Available event topics

        **Orders:** `orders`, `orders.created`, `orders.created.checkout`, `orders.updated`,
        `orders.updated.placed`, `orders.updated.closed`, `orders.updated.shipped`,
        `orders.updated.cancelled`, `orders.deleted`

        **Products:** `products`, `products.created`, `products.updated`, `products.deleted`,
        `products.updated.variants.created`, `products.updated.variants.updated`,
        `products.updated.variants.deleted`, `products.updated.assets.created`,
        `products.updated.assets.deleted`
      operationId: createSubscription
      parameters:
        - name: shop_id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubscriptionInput"
            example:
              topic: orders.updated.placed
              url: https://www.myapp.com/events
              notify_origin: true
              auth:
                type: basic
                username: foo
                password: bar
      responses:
        "201":
          description: Subscription created (pending verification)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscription"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /hub/{shop_id}/subscriptions/{id}:
    delete:
      tags: [Webhooks]
      summary: Unsubscribe
      description: Deletes a webhook subscription.
      operationId: deleteSubscription
      parameters:
        - name: shop_id
          in: path
          required: true
          schema:
            type: integer
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Subscription ID
      responses:
        "204":
          description: Subscription deleted
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Themes ────────────────────────────────────────────────────────────────
  /shops/{shop_id}/theme:
    get:
      tags: [Themes]
      summary: Theme details
      description: |
        Returns the active theme for a shop, including all templates and static assets.
        Requires `admin` scope.
      operationId: getTheme
      security:
        - bearerAuth: [admin]
      parameters:
        - $ref: "#/components/parameters/shop_id"
      responses:
        "200":
          description: Theme resource
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ThemeDetails"

  /themes/{id}:
    put:
      tags: [Themes]
      summary: Update theme settings
      description: Updates the settings object for a theme. Requires `admin` scope.
      operationId: updateTheme
      security:
        - bearerAuth: [admin]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Theme ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                settings:
                  type: object
                  additionalProperties: true
                  description: Theme settings key/value pairs (theme-specific)
      responses:
        "200":
          description: Theme updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ThemeDetails"

  /themes/{theme_id}/templates:
    post:
      tags: [Themes]
      summary: Create template
      description: |
        Adds a new HTML, CSS or JavaScript template to a theme. The `file_name` extension
        determines the content type (`html`, `css`, `js`). Requires `admin` scope.
      operationId: createTemplate
      security:
        - bearerAuth: [admin]
      parameters:
        - name: theme_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TemplateInput"
            example:
              file_name: about.html
              body: "<h1>About Us</h1>"
      responses:
        "201":
          description: Template created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Template"

  /themes/{theme_id}/templates/{id}:
    put:
      tags: [Themes]
      summary: Update template
      description: Updates the body of an existing theme template. Requires `admin` scope.
      operationId: updateTemplate
      security:
        - bearerAuth: [admin]
      parameters:
        - name: theme_id
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Template ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TemplateInput"
      responses:
        "200":
          description: Template updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Template"

    delete:
      tags: [Themes]
      summary: Delete template
      description: |
        Removes a template from a theme. Some core templates (e.g. `index.html`) cannot be deleted.
        Requires `admin` scope.
      operationId: deleteTemplate
      security:
        - bearerAuth: [admin]
      parameters:
        - name: theme_id
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Template deleted
        "404":
          $ref: "#/components/responses/NotFound"

  /themes/{theme_id}/assets:
    post:
      tags: [Themes]
      summary: Upload theme asset
      description: |
        Uploads a new static asset (image, font, etc.) to a theme. The file data must be
        Base64-encoded. Requires `admin` scope.
      operationId: createThemeAsset
      security:
        - bearerAuth: [admin]
      parameters:
        - name: theme_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ThemeAssetInput"
            example:
              file_name: logo.png
              data: "data:image/png;base64,iVBORw0KGgoAAAANS..."
      responses:
        "201":
          description: Asset uploaded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ThemeAsset"

  /themes/{theme_id}/assets/{id}:
    delete:
      tags: [Themes]
      summary: Delete theme asset
      description: Permanently removes a static asset from a theme. Requires `admin` scope.
      operationId: deleteThemeAsset
      security:
        - bearerAuth: [admin]
      parameters:
        - name: theme_id
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Asset ID
      responses:
        "204":
          description: Asset deleted
        "404":
          $ref: "#/components/responses/NotFound"

# ── Components ──────────────────────────────────────────────────────────────
components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        OAuth 2.0 Bearer token. Obtain via `POST https://auth.onbolder.com/oauth/token`.

  parameters:
    shop_id:
      name: shop_id
      in: path
      required: true
      schema:
        type: string
      description: Shop ID or subdomain
    page:
      name: page
      in: query
      schema:
        type: integer
        default: 1
      description: Page number
    per_page:
      name: per_page
      in: query
      schema:
        type: integer
        default: 50
      description: Items per page

  responses:
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    UnprocessableEntity:
      description: Validation errors
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ValidationErrors"
          example:
            _class: [errors]
            _embedded:
              errors:
                - _class: [error]
                  field: product.title
                  messages: ["can't be blank"]

  schemas:

    # ── Shared ──────────────────────────────────────────────────────────────
    HalLink:
      type: object
      properties:
        href:
          type: string
        method:
          type: string
        title:
          type: string
        type:
          type: string
        templated:
          type: boolean

    PaginatedResults:
      type: object
      properties:
        total_items:
          type: integer
        page:
          type: integer
        per_page:
          type: integer
        _links:
          type: object
          properties:
            self:
              $ref: "#/components/schemas/HalLink"
            next:
              $ref: "#/components/schemas/HalLink"
            last:
              $ref: "#/components/schemas/HalLink"

    ErrorResponse:
      type: object
      properties:
        message:
          type: string

    ValidationErrors:
      type: object
      properties:
        _class:
          type: array
          items:
            type: string
        _embedded:
          type: object
          properties:
            errors:
              type: array
              items:
                type: object
                properties:
                  _class:
                    type: array
                    items:
                      type: string
                  field:
                    type: string
                  messages:
                    type: array
                    items:
                      type: string

    ImageLinks:
      type: object
      description: Links to image at various sizes
      properties:
        thumbnail:
          $ref: "#/components/schemas/HalLink"
        small:
          $ref: "#/components/schemas/HalLink"
        medium:
          $ref: "#/components/schemas/HalLink"
        large:
          $ref: "#/components/schemas/HalLink"
        original:
          $ref: "#/components/schemas/HalLink"

    # ── Root ────────────────────────────────────────────────────────────────
    RootResource:
      type: object
      properties:
        uid:
          type: integer
        email:
          type: string
        user_name:
          type: string
        scopes:
          type: string
        last_logged_on:
          type: string
          format: date-time
        _links:
          type: object
        _embedded:
          type: object
          properties:
            shops:
              type: array
              items:
                $ref: "#/components/schemas/ShopSummary"

    # ── Shops ────────────────────────────────────────────────────────────────
    ShopSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        subdomain:
          type: string
        url:
          type: string
        description:
          type: string
        currency_code:
          type: string
        locale:
          type: string
        created_on:
          type: string
          format: date-time
        updated_on:
          type: string
          format: date-time
        product_counts:
          type: object
          properties:
            visible:
              type: integer
            hidden:
              type: integer
        _links:
          type: object

    ShopDetails:
      allOf:
        - $ref: "#/components/schemas/ShopSummary"
        - type: object
          properties:
            analytics_id:
              type: string
              nullable: true
            asset_count:
              type: integer
            _embedded:
              type: object
              properties:
                account:
                  type: object
                  properties:
                    status:
                      type: string
                    plan:
                      type: string
                    created_on:
                      type: string
                      format: date-time
                    updated_on:
                      type: string
                      format: date-time
                branches:
                  type: array
                  items:
                    $ref: "#/components/schemas/Branch"

    ShopResults:
      allOf:
        - $ref: "#/components/schemas/PaginatedResults"
        - type: object
          properties:
            _embedded:
              type: object
              properties:
                items:
                  type: array
                  items:
                    $ref: "#/components/schemas/ShopSummary"

    Branch:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        type:
          type: string
        allow_local_pickup:
          type: boolean
        _embedded:
          type: object
          properties:
            address:
              $ref: "#/components/schemas/Address"

    # ── Products ─────────────────────────────────────────────────────────────
    ProductSummary:
      type: object
      properties:
        id:
          type: string
        slug:
          type: string
        title:
          type: string
        status:
          type: string
          enum: [visible, hidden]
        price:
          type: integer
          description: Price in cents
        price_comparison:
          type: integer
          nullable: true
          description: "Original/comparison price in cents (\"was\" price)"
        discount_percentage:
          type: number
          nullable: true
        currency_code:
          type: string
        vendor:
          type: string
        tags:
          type: array
          items:
            type: string
        created_on:
          type: string
          format: date-time
        updated_on:
          type: string
          format: date-time
        _links:
          type: object
        _embedded:
          type: object
          properties:
            shop:
              $ref: "#/components/schemas/ShopSummary"
            image:
              $ref: "#/components/schemas/ProductImage"
            variants:
              type: array
              items:
                $ref: "#/components/schemas/VariantSummary"
            collections:
              type: array
              items:
                $ref: "#/components/schemas/Collection"

    ProductDetails:
      allOf:
        - $ref: "#/components/schemas/ProductSummary"
        - type: object
          properties:
            description:
              type: string
            _embedded:
              type: object
              properties:
                variants:
                  type: array
                  items:
                    $ref: "#/components/schemas/VariantDetails"
                images:
                  type: array
                  items:
                    $ref: "#/components/schemas/ProductImage"
                collections:
                  type: array
                  items:
                    $ref: "#/components/schemas/Collection"
                custom_attributes:
                  type: array
                  items:
                    $ref: "#/components/schemas/CustomAttribute"

    ProductResults:
      allOf:
        - $ref: "#/components/schemas/PaginatedResults"
        - type: object
          properties:
            _embedded:
              type: object
              properties:
                items:
                  type: array
                  items:
                    $ref: "#/components/schemas/ProductSummary"

    ProductInput:
      type: object
      properties:
        title:
          type: string
          description: Product title. Required on create.
        price:
          type: integer
          description: Price in cents
        price_comparison:
          type: integer
          description: "\"Was\" price in cents"
        description:
          type: string
        status:
          type: string
          enum: [visible, hidden]
          default: hidden
        product_type_name:
          type: string
          description: Name for new or existing product type
        vendor:
          type: string
        tags:
          type: array
          items:
            type: string
        collections:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
                description: Assign existing collection by ID
              title:
                type: string
                description: Create and assign new collection by title
        variants:
          type: array
          items:
            $ref: "#/components/schemas/VariantInput"
        custom_attributes:
          type: array
          items:
            $ref: "#/components/schemas/CustomAttributeInput"

    ProductImage:
      type: object
      properties:
        file_name:
          type: string
        title:
          type: string
        _links:
          $ref: "#/components/schemas/ImageLinks"

    ProductAssetDetails:
      type: object
      properties:
        file_name:
          type: string
        title:
          type: string
        description:
          type: string
        _links:
          allOf:
            - $ref: "#/components/schemas/ImageLinks"
            - type: object
              properties:
                btc:delete_product_asset:
                  $ref: "#/components/schemas/HalLink"

    ProductAssetInput:
      type: object
      required: [data, file_name]
      properties:
        data:
          type: string
          description: Base64-encoded image data URI (`data:image/jpeg;base64,...`)
        file_name:
          type: string
        title:
          type: string
        description:
          type: string

    Collection:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        slug:
          type: string
        shop_id:
          type: integer

    CustomAttribute:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        value:
          type: string
        field_type:
          type: string
          enum: [string, email, url, int, boolean]
        required:
          type: boolean
        key:
          type: string

    CustomAttributeInput:
      type: object
      required: [name, value]
      properties:
        name:
          type: string
        value:
          type: string
        field_type:
          type: string
          enum: [string, email, url, int, boolean]
          default: string
        required:
          type: boolean
          default: false

    # ── Variants ─────────────────────────────────────────────────────────────
    VariantSummary:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        sku:
          type: string
          nullable: true
        price:
          type: integer
        price_comparison:
          type: integer
          nullable: true
        stock:
          type: integer
        available_if_no_stock:
          type: boolean
        weight_in_grams:
          type: integer

    VariantDetails:
      allOf:
        - $ref: "#/components/schemas/VariantSummary"
        - type: object
          properties:
            _links:
              type: object

    VariantInput:
      type: object
      properties:
        title:
          type: string
          description: Required on create
        sku:
          type: string
          description: Must be unique within the shop
        stock:
          type: integer
          default: 1
        available_if_no_stock:
          type: integer
          enum: [0, 1]
          default: 1
          description: "`1` = always available, `0` = unavailable when out of stock"
        weight_in_grams:
          type: integer
        price:
          type: integer
          description: Override product price for this variant (cents)
        price_comparison:
          type: integer
          description: Override comparison price for this variant (cents)

    # ── Orders ───────────────────────────────────────────────────────────────
    OrderSummary:
      type: object
      properties:
        id:
          type: integer
        shop_id:
          type: integer
        code:
          type: string
        status:
          type: string
          enum: [checkout, pending, closed, shipped, invalid]
        net_total:
          type: integer
          description: Order subtotal in cents (before shipping/discounts)
        total:
          type: integer
          description: Final total in cents
        shipping_total:
          type: integer
        shipping_description:
          type: string
        discount_total:
          type: integer
        tracking_code:
          type: string
          nullable: true
        courier_name:
          type: string
          nullable: true
        total_weight_in_grams:
          type: integer
        created_on:
          type: string
          format: date-time
        updated_on:
          type: string
          format: date-time
        pending_on:
          type: string
          format: date-time
          nullable: true
        closed_on:
          type: string
          format: date-time
          nullable: true
        shipped_on:
          type: string
          format: date-time
          nullable: true
        fulfillment:
          type: object
          properties:
            fulfillment_method:
              type: string
              enum: [shipping, pickup]
        payment_info:
          type: object
          properties:
            type:
              type: string
            name:
              type: string
            auth_code:
              type: string
            transaction_id:
              type: string
        _links:
          type: object

    OrderDetails:
      allOf:
        - $ref: "#/components/schemas/OrderSummary"
        - type: object
          properties:
            _embedded:
              type: object
              properties:
                contact:
                  $ref: "#/components/schemas/ContactSummary"
                address:
                  $ref: "#/components/schemas/Address"
                line_items:
                  type: array
                  items:
                    $ref: "#/components/schemas/LineItem"
                promotion:
                  $ref: "#/components/schemas/Promotion"
                discounts:
                  type: array
                  items:
                    type: object

    OrderResults:
      allOf:
        - $ref: "#/components/schemas/PaginatedResults"
        - type: object
          properties:
            _embedded:
              type: object
              properties:
                items:
                  type: array
                  items:
                    $ref: "#/components/schemas/OrderSummary"

    OrderInput:
      type: object
      properties:
        line_items:
          type: array
          items:
            $ref: "#/components/schemas/LineItemInput"
        contact:
          $ref: "#/components/schemas/ContactInput"
        address:
          $ref: "#/components/schemas/AddressInput"

    OrderUpdateInput:
      type: object
      properties:
        line_items:
          type: array
          description: "Pass `units: -1` to remove a variant from the order"
          items:
            $ref: "#/components/schemas/LineItemInput"
        contact:
          $ref: "#/components/schemas/ContactInput"
        address:
          type: object
          properties:
            id:
              type: integer
              description: Copy address attributes from an existing address ID

    OrderBatchResult:
      type: object
      properties:
        ids:
          type: array
          items:
            type: integer
        additions:
          type: object
        removals:
          type: object
        _embedded:
          type: object
          properties:
            valid_items:
              type: array
              items:
                $ref: "#/components/schemas/OrderSummary"
            invalid_items:
              type: array
              items:
                type: object

    LineItem:
      type: object
      properties:
        product_id:
          type: integer
        product_title:
          type: string
        unit_price:
          type: integer
        units:
          type: integer
        discount_total:
          type: integer
        net_total:
          type: integer
        total:
          type: integer
        variant_id:
          type: integer
        variant_title:
          type: string
        variant_sku:
          type: string
        weight_in_grams:
          type: integer

    LineItemInput:
      type: object
      required: [variant_id, units]
      properties:
        variant_id:
          type: integer
        units:
          type: integer
          description: Number of units. Pass `-1` to remove this variant from the order.

    Promotion:
      type: object
      properties:
        name:
          type: string
        discount:
          type: integer
        discount_type:
          type: string
          enum: ["$", "%"]
        free_shipping:
          type: boolean

    # ── Contacts ─────────────────────────────────────────────────────────────
    ContactSummary:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
        phone_number:
          type: string

    ContactInput:
      type: object
      required: [email]
      properties:
        email:
          type: string
          description: Used to look up an existing contact. Required.
        name:
          type: string
          description: Required for new contacts
        phone_number:
          type: string
          description: Required for new contacts
        company:
          type: string

    ContactFull:
      allOf:
        - $ref: "#/components/schemas/ContactSummary"
        - type: object
          properties:
            company:
              type: string
            opt_in:
              type: boolean
            available_orders_count:
              type: integer
            created_on:
              type: string
              format: date-time
            updated_on:
              type: string
              format: date-time

    ContactResults:
      allOf:
        - $ref: "#/components/schemas/PaginatedResults"
        - type: object
          properties:
            _embedded:
              type: object
              properties:
                items:
                  type: array
                  items:
                    $ref: "#/components/schemas/ContactFull"

    # ── Address ───────────────────────────────────────────────────────────────
    Address:
      type: object
      properties:
        street:
          type: string
        street_2:
          type: string
        locality_name:
          type: string
        region_name:
          type: string
        country_name:
          type: string
        country_code:
          type: string
        postal_code:
          type: string
          nullable: true
        lat_long:
          type: string
          description: "Latitude,longitude pair"

    AddressInput:
      type: object
      properties:
        street:
          type: string
        street_2:
          type: string
        locality_name:
          type: string
        region_name:
          type: string
        country_code:
          type: string
        postal_code:
          type: string

    # ── Webhooks ──────────────────────────────────────────────────────────────
    HubResource:
      type: object
      properties:
        _links:
          type: object

    Subscription:
      type: object
      properties:
        id:
          type: integer
        status:
          type: string
          enum: [pending, active, failed_activation, failed, disabled]
        topic:
          type: string
        url:
          type: string
        notify_origin:
          type: boolean
        auth:
          $ref: "#/components/schemas/SubscriptionAuth"
        error_count:
          type: integer
        last_error:
          type: string
          nullable: true
        created_on:
          type: string
          format: date-time
        updated_on:
          type: string
          format: date-time
        _links:
          type: object

    SubscriptionInput:
      type: object
      required: [topic, url]
      properties:
        topic:
          type: string
          description: "Event topic to subscribe to, e.g. `orders.updated.placed`"
        url:
          type: string
          description: URL to POST event notifications to
        notify_origin:
          type: boolean
          default: true
          description: Whether to notify the originating app
        auth:
          $ref: "#/components/schemas/SubscriptionAuth"

    SubscriptionAuth:
      type: object
      properties:
        type:
          type: string
          enum: [none, basic]
        username:
          type: string
        password:
          type: string

    SubscriptionResults:
      allOf:
        - $ref: "#/components/schemas/PaginatedResults"
        - type: object
          properties:
            _embedded:
              type: object
              properties:
                items:
                  type: array
                  items:
                    $ref: "#/components/schemas/Subscription"

    # ── Themes ────────────────────────────────────────────────────────────────
    ThemeDetails:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        production:
          type: boolean
        settings:
          type: object
          additionalProperties: true
        updated_on:
          type: string
          format: date-time
        _links:
          type: object
        _embedded:
          type: object
          properties:
            templates:
              type: array
              items:
                $ref: "#/components/schemas/Template"
            assets:
              type: array
              items:
                $ref: "#/components/schemas/ThemeAsset"

    Template:
      type: object
      properties:
        id:
          type: string
        file_name:
          type: string
        name:
          type: string
        content_type:
          type: string
          enum: [html, css, js]
        body:
          type: string
        updated_on:
          type: string
          format: date-time
        _links:
          type: object

    TemplateInput:
      type: object
      required: [file_name, body]
      properties:
        file_name:
          type: string
          description: "File name including extension, e.g. `about.html`"
        body:
          type: string
          description: Template content

    ThemeAsset:
      type: object
      properties:
        id:
          type: string
        file_name:
          type: string
        content_type:
          type: string
        file_size:
          type: integer
        digest:
          type: string
        updated_on:
          type: string
          format: date-time
        _links:
          type: object
          properties:
            file:
              $ref: "#/components/schemas/HalLink"

    ThemeAssetInput:
      type: object
      required: [file_name, data]
      properties:
        file_name:
          type: string
        data:
          type: string
          description: Base64-encoded file data URI
