> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.nowbookit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ingest Sale

> Submit a POS sale transaction in NowBookIt's canonical shape.

Submits a sale transaction, optionally linked to an existing booking via `bookingId`. The request is validated immediately; matching and storing the sale happens moments later, so a `202` confirms the payload was accepted, not that the sale is stored yet.

> **Validation rules**
>
> * Either `tables` or `bookingId` must be provided.
> * `closedAt` is required, and must be after `openedAt`, when `recordType` is `Final`.
> * `transactionType: Refund` requires `originalSaleId`; `Sale`/`Void` must not set it.
> * Each item's `total` must equal `unitPrice × quantity − discount`.
> * When `payments` is included, the amounts must sum to `total` (which excludes `tip`/`surcharge` — those are separate fields).
> * `source.other` is required when `source.type` is `Other`.

> **recordType: Final, Tally, or Incremental**
>
> * **`Final`** — the sale is closed/settled. Resubmitting the same `partnerSaleId` is a no-op.
> * **`Incremental`** — the record accumulates onto an existing `partnerSaleId`: amounts add up, and items/payments append.
> * **`Tally`** — a full running snapshot of a still-open sale, resent periodically as it changes. The first submission for a `partnerSaleId` inserts it; each subsequent resend is diffed against the stored record and only the changed fields are applied.

> **Booking reconciliation**
>
> After a sale is stored, NowBookIt tries to match it to a booking — by `bookingId` if you sent one, otherwise by table name and close time. On a match the sale is enriched with the booking's service, table sections, and diner details. See [List Sales](/platform/sales/list-sales) for how the outcome is reported.

### Example

```shell curl theme={null}
curl --location 'https://platform.uat.nowbookit.com/api/sales/transactions' \
--header 'Authorization: Bearer your_api_key_here' \
--header 'Content-Type: application/json' \
--data '{
  "partnerSaleId": "pos-sale-9981",
  "source": { "type": "Pos" },
  "transactionType": "Sale",
  "openedAt": "2026-07-23T19:05:00Z",
  "closedAt": "2026-07-23T20:45:00Z",
  "bookingId": "abc-123",
  "tables": [{ "name": "12", "seats": 4 }],
  "staffId": "staff-7",
  "items": [
    {
      "id": "item-1",
      "name": "Wagyu Steak",
      "quantity": 2,
      "unitPrice": 6500,
      "unitTax": 591,
      "total": 13000
    }
  ],
  "payments": [{ "id": "pay-1", "type": "EFTPOS", "amount": 18500 }],
  "total": 18500,
  "currency": "AUD"
}'
```

```json Response — 202 Accepted theme={null}
{
  "accepted": true,
  "partnerSaleId": "pos-sale-9981"
}
```


## OpenAPI

````yaml platform/sales/openapi.json POST /api/sales/transactions
openapi: 3.0.4
info:
  title: NowBookIt Sales API
  description: >-
    REST API for submitting POS sale transactions to NowBookIt and retrieving a
    venue's sales history.
  version: v1
  contact:
    email: platform.integrations@nowbookit.com
servers:
  - url: https://platform.uat.nowbookit.com
    description: UAT
  - url: https://platform.nowbookit.com
    description: Production
security:
  - ApiKeyAuth: []
tags:
  - name: Sales
    description: POS sale ingest and venue sales listing.
paths:
  /api/sales/transactions:
    post:
      tags:
        - Sales
      summary: Ingest a POS sale
      description: >-
        Validates the sale payload and enqueues it for asynchronous ingestion.
        Returns `202 Accepted` immediately — this is **not** a guarantee the
        sale will be written; new/duplicate/rejected is decided later,
        asynchronously, and is never reported back to the caller. Idempotency
        key is `partnerId:venueId:partnerSaleId`.
      operationId: ingestSale
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SaleRequest'
            example:
              partnerSaleId: pos-sale-9981
              source:
                type: Pos
              transactionType: Sale
              recordType: Final
              openedAt: '2026-07-23T19:05:00Z'
              closedAt: '2026-07-23T20:45:00Z'
              bookingId: abc-123
              tables:
                - name: '12'
                  seats: 4
              staffId: staff-7
              items:
                - id: item-1
                  name: Wagyu Steak
                  category: food
                  quantity: 2
                  unitPrice: 6500
                  unitTax: 591
                  total: 13000
              payments:
                - id: pay-1
                  type: EFTPOS
                  amount: 18500
              total: 18500
              currency: AUD
      responses:
        '202':
          description: Validated and enqueued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SaleIngestAccepted'
        '400':
          description: Malformed JSON body, or schema/cross-field validation failure.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - $ref: '#/components/schemas/ValidationErrorResponse'
              examples:
                validationFailure:
                  summary: Schema/cross-field validation failure
                  value:
                    code: ValidationError
                    message: Request body failed validation
                    requestId: f21151ab-7139-46c5-97c3-cf63b8a64af5
                    errors:
                      transactionType:
                        - 'Invalid input: expected string, received undefined'
                malformedJson:
                  summary: Malformed JSON body
                  value:
                    code: AppError
                    message: Unexpected token in JSON at position 42
                    requestId: b3d6a1e2-4f9c-4a2d-8b0a-example
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: UnauthorisedError
                message: Invalid API key
                requestId: 8f14e45f-ceea-4c6f-8f89-example
        '502':
          description: >-
            The iPOS API-key resolve endpoint was unavailable or returned an
            unexpected response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: AppError
                message: iPOS API-key resolve endpoint returned an unexpected response
                requestId: 6e2a9c4f-1d3b-4a7e-9c5f-example
components:
  schemas:
    SaleRequest:
      type: object
      description: >-
        Canonical POS sale payload. Cross-field rules enforced server-side:


        • `source.other` is required when `source.type` is `Other`.

        • `originalSaleId` is required (and must differ from `partnerSaleId`)
        when `transactionType` is `Refund`; forbidden for `Sale`/`Void`.

        • `total` must be non-negative.

        • `closedAt` must be strictly after `openedAt`, and is required when
        `recordType` is `Final`.

        • At least one of `tables` or `bookingId` is required.

        • Each item's `total` must reconcile.

        • When `payments` is present, the amounts must sum to `total`.


        Explicit JSON `null` on any field (including nested) is treated as
        absent.
      properties:
        partnerSaleId:
          type: string
          minLength: 1
          description: POS-generated sale ID. Idempotency key alongside partner/venue.
        originalSaleId:
          type: string
          description: >-
            Required when `transactionType` is `Refund`; must not be set
            otherwise.
        posLocationId:
          type: string
          description: The venue or location ID as known to your POS system.
        source:
          $ref: '#/components/schemas/SaleSourceInput'
        transactionType:
          type: string
          enum:
            - Sale
            - Refund
            - Void
          description: Case-insensitive; normalised to canonical casing.
        recordType:
          type: string
          enum:
            - Final
            - Tally
            - Incremental
          default: Final
          description: >-
            Case-insensitive. `Final` is the closed/settled sale — resubmitting
            the same `partnerSaleId` is a no-op. `Incremental` accumulates onto
            an existing record — amounts add, items/payments append. `Tally` is
            a full running snapshot of a still-open sale; the first submission
            for a `partnerSaleId` inserts it, and each resend is diffed against
            the stored record so only the changed fields are applied (a delta
            update, not a full replace).
        posStatus:
          type: string
          description: Raw POS status string, passed through as-is.
        bookingId:
          type: string
          description: >-
            NBI booking ID. Blank/whitespace normalises to absent. Either this
            or `tables` is required.
        openedAt:
          type: string
          format: date-time
          description: Any UTC offset accepted; normalised to UTC on write.
        closedAt:
          type: string
          format: date-time
          description: >-
            Required when `recordType` is `Final`. Must be strictly after
            `openedAt`.
        tables:
          type: array
          items:
            $ref: '#/components/schemas/SaleTableInput'
          description: >-
            Either this or `bookingId` is required. Section info is never
            accepted here — only reconciliation populates per-table section.
        covers:
          type: integer
          description: Number of guests at the table.
          minimum: 1
        register:
          $ref: '#/components/schemas/RegisterInput'
        staffId:
          type: string
          minLength: 1
          description: Required — every sale must be attributable to a staff member.
        staffName:
          type: string
          description: Name of the staff member who processed the sale.
        diner:
          $ref: '#/components/schemas/DinerInput'
        items:
          type: array
          description: The food, drinks, and other items included in the sale.
          items:
            $ref: '#/components/schemas/SaleItemInput'
        payments:
          type: array
          description: The payments made toward the sale.
          items:
            $ref: '#/components/schemas/SalePaymentInput'
        total:
          type: integer
          description: >-
            Goods + tax only. Excludes `tip` and `surcharges`, which are
            separate additive fields. Must be >= 0.
        subtotal:
          type: integer
          description: Amount in whole cents (no decimals).
        tax:
          type: integer
          description: Amount in whole cents (no decimals).
        discount:
          type: integer
          description: Amount in whole cents (no decimals).
        surcharges:
          type: integer
          description: Amount in whole cents (no decimals).
        tip:
          type: integer
          description: Amount in whole cents (no decimals).
        currency:
          type: string
          description: Three-letter currency code, e.g. AUD or USD.
          minLength: 3
          maxLength: 3
          example: AUD
      required:
        - partnerSaleId
        - source
        - transactionType
        - openedAt
        - staffId
        - total
        - currency
    SaleIngestAccepted:
      type: object
      description: >-
        Confirms the request was validated and enqueued — not that the sale was
        accepted as new. Actual ingestion (duplicate/accumulate/reject decision)
        happens asynchronously; the caller is never told the outcome
        synchronously.
      properties:
        accepted:
          type: boolean
          description: Whether the sale was accepted for processing.
          example: true
        partnerSaleId:
          type: string
          description: The sale ID you submitted, echoed back for reference.
      required:
        - accepted
        - partnerSaleId
    ErrorResponse:
      type: object
      properties:
        code:
          type: string
          description: >-
            Error class name, e.g. `UnauthorisedError`, `ValidationError`,
            `AppError`.
          example: UnauthorisedError
        message:
          type: string
          description: What went wrong, described in plain language.
          example: Invalid API key
        requestId:
          type: string
          description: >-
            A unique reference number for this request — handy if you need to
            contact support about it.
          example: 8f14e45f-ceea-4c6f-8f89-example
      required:
        - code
        - message
        - requestId
    ValidationErrorResponse:
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          properties:
            errors:
              type: object
              description: >-
                Per-field validation messages, keyed by dotted path (`body` or
                `query` for root-level cross-field issues).
              additionalProperties:
                type: array
                items:
                  type: string
              example:
                total:
                  - Sum of payments amounts (1800) must equal total (1850)
                closedAt:
                  - closedAt is required when recordType is Final
          required:
            - errors
    SaleSourceInput:
      type: object
      description: Origin of the sale. `other` is required when `type` is `Other`.
      properties:
        type:
          type: string
          enum:
            - Pos
            - Crm
            - Functions
            - Channels
            - RMS
            - PaymentTerminal
            - Other
          description: Case-insensitive; normalised to canonical casing.
        other:
          type: string
          description: Free-text elaboration. Required when `type` is `Other`.
      required:
        - type
    SaleTableInput:
      description: >-
        A plain table name, or an object carrying its own seats count. Blank
        names are dropped.
      oneOf:
        - type: string
          example: '12'
        - type: object
          properties:
            name:
              type: string
              description: Table name or number, as used on the floor plan.
            seats:
              type: integer
              description: Number of seats at this table.
              minimum: 1
          required:
            - name
    RegisterInput:
      type: object
      properties:
        id:
          type: string
          description: ID of the till or register the sale was rung up on.
        name:
          type: string
          description: Name of the till or register, if it has one.
      required:
        - id
    DinerInput:
      type: object
      description: >-
        `id` is the NBI-resolved diner id (maps to the stored `dinerId`,
        defaults to `unknown` when blank/absent). `partnerDinerId` is the
        partner's own external customer id, kept separate from `id`.
      properties:
        id:
          type: string
          description: The diner's ID in NowBookIt, once known.
        partnerDinerId:
          type: string
          description: The diner's ID in your own system, if different from NowBookIt's.
        firstName:
          type: string
          description: Diner's first name.
        lastName:
          type: string
          description: Diner's last name.
        email:
          type: string
          description: Diner's email address.
          format: email
        phone:
          type: string
          description: Diner's phone number.
        notes:
          type: string
          description: Any notes about the diner.
    SaleItemInput:
      type: object
      description: '`total` must equal `unitPrice * quantity - discount` (1-cent tolerance).'
      properties:
        id:
          type: string
          description: Your own ID for this item on the sale.
        sku:
          type: string
          description: The item's SKU or product code, if it has one.
        name:
          type: string
          description: Name of the item, as it appears on the receipt.
        category:
          type: string
          description: Category the item belongs to, e.g. food, drinks, or merchandise.
        barcode:
          type: string
          description: Barcode for the item, if it was scanned.
        quantity:
          type: number
          description: How many of this item were ordered.
        unitPrice:
          type: integer
          description: Amount in whole cents (no decimals).
        unitTax:
          type: integer
          description: Amount in whole cents (no decimals).
        discount:
          type: integer
          description: Amount in whole cents (no decimals).
        total:
          type: integer
          description: Amount in whole cents (no decimals).
      required:
        - id
        - name
        - quantity
        - unitPrice
        - total
    SalePaymentInput:
      type: object
      description: >-
        When `payments` is present, `sum(payments[].amount)` must equal `total`
        (1-cent tolerance). `amount` excludes `tip`/`surcharge`.
      properties:
        id:
          type: string
          description: Your own ID for this payment.
        type:
          type: string
          enum:
            - CASH
            - EFTPOS
            - CREDIT_CARD
            - GIFT_CARD
            - VOUCHER
            - ACCOUNT
            - DEPOSIT
            - BANK_TRANSFER
            - THIRD_PARTY
            - COMPLIMENTARY
            - OTHER
          description: Case-insensitive; normalised to canonical casing.
        status:
          type: string
          description: Status of the payment, e.g. approved or declined.
        amount:
          type: integer
          description: Amount in whole cents (no decimals).
        tip:
          type: integer
          description: Amount in whole cents (no decimals).
        surcharge:
          type: integer
          description: Amount in whole cents (no decimals).
        terminalId:
          type: string
          description: ID of the card machine or payment terminal used, if any.
        transactionReference:
          type: string
          description: Reference number for the payment transaction, if available.
      required:
        - id
        - type
        - amount
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: >-
        Your API key. See [Authorization](/platform/authorization) to create
        one.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.