openapi: 3.1.0

info:
  title: Kick a tin Along Partner API
  version: "1.0"
  summary: Product catalogue, quoting and registration for partner integrations.
  description: |
    This API gives you programmatic access to your supplier's product catalogue — including
    variants, live stock, tiered pricing and branding options — plus the ability to raise a quote.

    ## Signing in

    You are signed in, so every example below is already pointed at your own API address and the
    **Test Request** button carries your access token — you can run any endpoint here against
    your live data without setting anything up.

    Your session lasts 24 hours and is held only in this browser tab. Closing the tab signs you
    out, as does the **Sign out** button.

    There is no self-service sign-up. If a colleague needs access, speak to your account manager.

    ## Your API address

    Your account has its own API address, and **the address determines whose data you get**.
    There is no account parameter in any request — the address carries it.

    It was issued when your account was created and looks like
    `https://apiyourcompany.kickatinalong.net`. If you do not have it to hand, ask your account
    manager.

    ## Authenticating

    Two steps, and the token is what identifies you:

    1. `POST /api/Customer/Login` with your email and password to receive a token.
    2. Send it as `Authorization: Bearer <token>` on every subsequent request.

    Tokens are valid for **24 hours**. There is no refresh endpoint — sign in again when it
    expires. The panel on this page does both steps for you.

    If your email address is registered against more than one company, login returns
    `result: "multiple"` and a list of companies instead of a token. Repeat the call with the
    `customerId` of the one you want. This is the single most common integration surprise, so it
    is worth handling on day one. The sign-in panel will simply ask you which company to use.

    ## Your token decides what you can see

    The catalogue is not a public price list. Products appear in your feed only where your
    account has a supplier mapping, and the prices returned are **yours** — your negotiated
    discount and your supplier's markup are already applied. Two customers calling the same
    endpoint will legitimately receive different products at different prices.

    A consequence worth knowing: an empty `[]` means "nothing is mapped to your account", not
    "there are no products". If you get an empty feed, the mapping is what to check.

    ## Errors

    Every failure returns JSON. A `401` carries a machine-readable `code` so you can tell the
    cases apart without parsing prose:

    | code | meaning | what to do |
    |---|---|---|
    | `NOT_AUTHENTICATED` | no `Authorization` header was sent | send the bearer token |
    | `SESSION_EXPIRED` | the token's lifetime has passed | log in again |
    | `INVALID_TOKEN` | the token was rejected — signature, format, or wrong audience | log in again; if it repeats, contact support |

    A `400` returns a plain-text message describing what was wrong with the request.

    ## Caching and rate limits

    Feed responses are cached server-side for **one hour**, per access token. A catalogue change
    may take up to an hour to appear. Polling more often than hourly gains you nothing.

    Requests are limited to 1,000 per second and 50,000 per minute per IP. Exceeding either
    returns `429`.

  contact:
    name: KATA Support
    email: support@kickatinalong.net

servers:
  # No customer addresses are listed here, and none ever should be: this page is public, so an
  # enum of real subdomains would publish the client list to anyone who opened it. The address
  # is a free-text variable that the sign-in panel fills in from what the reader types.
  #
  # The sandbox is listed first and concrete so the generated code samples are copy-pasteable.
  # A templated URL alone renders as a literal "{account}" in curl, which is the sort of thing
  # someone pastes into a terminal and then reports as a bug.
  - url: https://apitest.kickatinalong.net
    description: Sandbox — demonstration data, no account needed.
  - url: https://api{account}.kickatinalong.net
    description: Your own API address. Sign in at the top of the page and this is filled in for you.
    variables:
      account:
        default: test
        description: |
          The identifier in your API address — everything between `api` and `.kickatinalong.net`.
          Sign in above and it is set for you, or ask your account manager if you are unsure.

security:
  - bearerAuth: []

tags:
  - name: Authentication
    description: Obtaining a token. Start here.
  - name: Products
    description: Your supplier's catalogue, priced for your account.
  - name: Quotes
    description: Raising a quote.

paths:
  /api/Customer/Login:
    post:
      tags: [Authentication]
      summary: Exchange credentials for a token
      operationId: customerLogin
      security: []
      description: |
        Returns a JWT valid for 24 hours. Send it as `Authorization: Bearer <token>` on every
        other endpoint.

        **If your credentials are valid for more than one company**, no token is issued. The
        response is `result: "multiple"` and `message` contains a JSON-encoded array of
        `{id, company}`. Call again with the same email and password plus `customerId` set to
        your chosen `id`.

        Always send the password, including on the first call — the company list is only
        returned once the password has been verified.

        Note that `message` is a JSON *string*, not a nested object — you have to parse it a
        second time. That is a quirk of the current contract rather than a mistake in this page.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            examples:
              standard:
                summary: Normal login
                value:
                  email: partner@example.com
                  password: your-password
              pickCompany:
                summary: Second call, after a "multiple" response
                value:
                  email: partner@example.com
                  password: your-password
                  customerId: 8458
      responses:
        '200':
          description: |
            Always `200`, including when authentication fails — inspect `result` and `token`
            rather than relying on the status code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
              examples:
                success:
                  summary: Token issued
                  value:
                    token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                    result: "8458"
                    message: null
                multiple:
                  summary: Email registered to several companies
                  value:
                    token: null
                    result: multiple
                    message: '[{"id":8458,"company":"KATA Test"},{"id":1,"company":"Test Multiple"}]'
                invalid:
                  summary: Email or password not recognised
                  value:
                    token: null
                    result: invalid
                    message: null
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/Product/Feed:
    get:
      tags: [Products]
      summary: Retrieve the product catalogue
      operationId: getProductFeed
      description: |
        Returns every active product mapped to your account, with variants, stock, tiered pricing,
        branding options, images and documents.

        The full feed is large — a full catalogue can run to several megabytes. Fetch it once an
        hour at most (that is the cache lifetime), or pass `productId` for a single product.

        Prices are **net to you**: your supplier's markup and your account's discount are
        already applied. `Stock[].Pricings` is a quantity-break ladder — the applicable price is
        the tier with the highest `MinimumQuantity` at or below your order quantity.
      parameters:
        - name: productId
          in: query
          required: false
          description: Return a single product by its internal id. Omit for the full catalogue.
          schema:
            type: integer
            format: int32
          example: 1
      responses:
        '200':
          description: |
            The catalogue. An empty array means no products are mapped to your account —
            it does not mean there is no catalogue.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Product'
              examples:
                oneProduct:
                  $ref: '#/components/examples/ProductFeedExample'
                empty:
                  summary: Nothing mapped to this account
                  value: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/Quote/GenerateQuote:
    post:
      tags: [Quotes]
      summary: Raise a quote
      operationId: generateQuote
      description: |
        Creates a quote against the account your API address points to, and emails it to
        `customerContactEmail` as a PDF.

        `customer` and `cartProducts` are the only required fields, but a quote with no delivery
        detail or job name is rarely useful to the recipient.

        Money fields are per-unit, in your account's currency. `costPrice` is what you pay,
        `sellingPrice` is what appears on the quote.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteRequest'
            examples:
              minimal:
                summary: Smallest accepted request
                value:
                  customer: Test Company
                  cartProducts:
                    - productCode: TEST-PROD
                      quantity: 2
                      costPrice: 50
                      sellingPrice: 75
              typical:
                summary: Realistic quote
                value:
                  customer: Test Company
                  customerContactName: Test Customer
                  customerContactEmail: test.customer@example.com
                  jobName: Summer Campaign Giveaway
                  po: SAMPLE-PO
                  deliveryAddress: 123 Test Street, Test City, TC 12345
                  deliveryDate: '2026-08-01T10:00:00.000Z'
                  deadlineDate: '2026-08-10T18:00:00.000Z'
                  specialInstructions: Please handle with care.
                  deliveryCharge: 150
                  cartProducts:
                    - supplier: SampleSupplier
                      productCode: TEST-PROD
                      colour: Blue
                      size: L
                      quantity: 250
                      costPrice: 50
                      sellingPrice: 75
                      notes: Two-colour print, front left chest
      responses:
        '200':
          description: Quote created.
          content:
            application/json:
              schema:
                type: string
              example: QUOTE GENERATED
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/BadRequest'


components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: The `token` from `POST /api/Customer/Login`. Valid for 24 hours.

  responses:
    Unauthorized:
      description: |
        No token, an expired token, or a token that could not be validated. The `code` field
        distinguishes the three.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              summary: No Authorization header
              value:
                code: NOT_AUTHENTICATED
                message: You are not signed in. Please sign in and try again.
            expired:
              summary: Token past its lifetime
              value:
                code: SESSION_EXPIRED
                message: Your session has expired. Please sign in again.
            invalid:
              summary: Token rejected
              value:
                code: INVALID_TOKEN
                message: Your sign-in is no longer valid. Please sign in again.
    BadRequest:
      description: The request could not be processed. The body is a plain-text description.
      content:
        application/json:
          schema:
            type: string
          example: "Invalid request"

  schemas:
    Error:
      type: object
      properties:
        code:
          type: string
          enum: [NOT_AUTHENTICATED, SESSION_EXPIRED, INVALID_TOKEN]
        message:
          type: string

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          format: password
        customerId:
          type: integer
          format: int32
          description: Required only on the second call, after a `multiple` response.
        otp:
          type: string
          description: Reserved. Leave unset.

    LoginResponse:
      type: object
      properties:
        token:
          type: [string, 'null']
          description: The JWT. Null when no token was issued.
        result:
          type: [string, 'null']
          description: |
            `success` with a `token` when the credentials are correct; `multiple` when those
            credentials are valid for several companies; `wrong_password` or `invalid` when they
            are not. Treat anything without a `token` as a failed sign-in.

            `multiple` is only ever returned for a **correct** password — the company list is not
            disclosed to an unauthenticated caller.
        message:
          type: [string, 'null']
          description: |
            When `result` is `multiple`, a JSON-encoded array of `{id, company}` — parse it as
            JSON a second time.

    Product:
      type: object
      properties:
        Code:
          type: string
          description: The product code. Use this when quoting.
        Description:
          type: string
        FullDescription:
          type: string
        Category:
          type: string
        Images:
          type: array
          items:
            $ref: '#/components/schemas/ProductImage'
        Variants:
          type: array
          description: The colour / size / weight combinations this product comes in.
          items:
            $ref: '#/components/schemas/Variant'
        Stock:
          type: array
          description: Live stock levels and your net price ladder, per variant.
          items:
            $ref: '#/components/schemas/Stock'
        Branding:
          type: array
          description: Branding positions available on this product, and what they cost.
          items:
            $ref: '#/components/schemas/BrandingPosition'
        Documents:
          type: array
          description: Branding guideline documents, as URLs.
          items:
            type: string

    ProductImage:
      type: object
      properties:
        MediaType:
          type: string
          example: Image
        ImageUrl:
          type: string
        Colour:
          type: [string, 'null']
          description: The colour this image depicts, when it is colour-specific.

    Variant:
      type: object
      properties:
        Code:
          type: string
        Colour:
          $ref: '#/components/schemas/Colour'
        Size:
          $ref: '#/components/schemas/NamedCode'
        Weight:
          $ref: '#/components/schemas/NamedCode'

    Colour:
      type: object
      properties:
        Code:
          type: string
        Name:
          type: string
        Hex:
          type: [string, 'null']
          example: '#1B3A6B'

    NamedCode:
      type: object
      properties:
        Code:
          type: string
        Name:
          type: string
        SortOrder:
          type: [integer, 'null']
          format: int32

    Stock:
      type: object
      properties:
        ProductId:
          type: integer
          format: int32
        Colour:
          type: string
        Size:
          type: string
        Weight:
          type: [string, 'null']
        Supplier:
          type: string
        Stock:
          type: number
          description: Units on hand at the supplier.
        Pricings:
          type: array
          items:
            $ref: '#/components/schemas/PricingTier'

    PricingTier:
      type: object
      description: |
        A quantity break. Apply the tier with the highest `MinimumQuantity` at or below your
        order quantity.
      properties:
        MinimumQuantity:
          type: integer
          format: int32
        Quantity:
          type: integer
          format: int32
        Price:
          type: number
          description: Net unit price to you, markup and discount already applied.
        PricingType:
          type: string
          example: Net

    BrandingPosition:
      type: object
      properties:
        Position:
          type: string
          example: Front Left Chest
        PositionCode:
          type: string
        Brandings:
          type: array
          items:
            $ref: '#/components/schemas/Branding'

    Branding:
      type: object
      properties:
        BrandingName:
          type: string
          example: Embroidery
        Price:
          type: array
          items:
            $ref: '#/components/schemas/BrandingPrice'

    BrandingPrice:
      type: object
      properties:
        Description:
          type: [string, 'null']
        NoOfColors:
          type: integer
          format: int32
        ChargesPerLocation:
          type: [integer, 'null']
          format: int32
        PriceType:
          type: string
          description: |
            `Setup` is charged once per job; `Run` is charged per unit. Both appear for the same
            branding — add the setup once and the run price per item.
          enum: [Setup, Run]
        Price:
          type: array
          items:
            $ref: '#/components/schemas/BrandingPricingTier'

    BrandingPricingTier:
      type: object
      properties:
        MinimumQuantity:
          type: integer
          format: int32
        Price:
          type: number
        RepeatPrice:
          type: number
          description: Charged for a repeat of the same branding, where it differs from `Price`.
        UOM:
          type: [string, 'null']

    QuoteRequest:
      type: object
      required: [customer, cartProducts]
      properties:
        customer:
          type: string
          description: The company the quote is for.
        cartProducts:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/QuoteLine'
        customerContactName:
          type: string
        customerContactEmail:
          type: string
          format: email
          description: The quote PDF is emailed here.
        jobName:
          type: string
        po:
          type: string
        deliveryAddress:
          type: string
        delivery:
          type: string
        deliveryDate:
          type: string
          format: date-time
        deadlineDate:
          type: string
          format: date-time
        specialInstructions:
          type: string
        rep:
          type: string
        quoteNo:
          type: integer
          format: int32
          description: Leave unset — one is assigned.
        deliveryCharge:
          type: number
        artwork:
          type: number
        transport:
          type: number
        overtime:
          type: number
        discount:
          type: number
        rebate:
          type: integer
          format: int32
        deposit:
          type: number
        outworkCost:
          type: number
        outworkSell:
          type: number
        attachments:
          type: array
          items:
            type: string

    QuoteLine:
      type: object
      description: One line on the quote.
      properties:
        productCode:
          type: string
          description: The `Code` from the product feed.
        supplier:
          type: string
        quantity:
          type: integer
          format: int32
        costPrice:
          type: number
          description: Per-unit cost to you.
        sellingPrice:
          type: number
          description: Per-unit price shown on the quote.
        colour:
          type: string
        size:
          type: string
        weight:
          type: string
        notes:
          type: string
        discount:
          type: number
        sortIndex:
          type: integer
          format: int32
          description: Line order on the quote.
        meters:
          type: number
        width:
          type: number
        height:
          type: number
        artworkNotes:
          type: string
        attachments:
          type: array
          items:
            type: string
        artworkFiles:
          type: array
          items:
            type: string
        brandingDetails:
          type: array
          items:
            type: object



  examples:
    ProductFeedExample:
      summary: One product, abbreviated
      value:
        - Code: TSHIRT-001
          Description: Classic Cotton T-Shirt
          FullDescription: 180gsm combed cotton, set-in sleeves, ribbed collar.
          Category: Apparel
          Images:
            - MediaType: Image
              ImageUrl: https://cdn.example.net/products/tshirt-001-navy.jpg
              Colour: Navy
          Variants:
            - Code: TSHIRT-001
              Colour: { Code: NVY, Name: Navy, Hex: '#1B3A6B' }
              Size: { Code: L, Name: Large, SortOrder: 3 }
              Weight: { Code: null, Name: null, SortOrder: null }
          Stock:
            - ProductId: 4821
              Colour: Navy
              Size: Large
              Weight: null
              Supplier: SampleSupplier
              Stock: 1450
              Pricings:
                - MinimumQuantity: 1
                  Quantity: 1
                  Price: 89.5
                  PricingType: Net
                - MinimumQuantity: 100
                  Quantity: 100
                  Price: 76.25
                  PricingType: Net
          Branding:
            - Position: Front Left Chest
              PositionCode: FLC
              Brandings:
                - BrandingName: Embroidery
                  Price:
                    - Description: Up to 5000 stitches
                      NoOfColors: 4
                      ChargesPerLocation: 1
                      PriceType: Setup
                      Price:
                        - MinimumQuantity: 1
                          Price: 250
                          RepeatPrice: 0
                          UOM: Job
                    - Description: Up to 5000 stitches
                      NoOfColors: 4
                      ChargesPerLocation: 1
                      PriceType: Run
                      Price:
                        - MinimumQuantity: 1
                          Price: 18.5
                          RepeatPrice: 15
                          UOM: Unit
          Documents:
            - https://cdn.example.net/docs/branding-guidelines.pdf
