Till sales

Called with shop (retail) account keys. A key only sees its own account's data. Every example uses the same sample brand, order and item, so you can follow one item from call to call.

Base URL http://62.169.23.239:8001/api/v1Auth Bearer token3 endpoints

Record a till sale#

POST/retail/pos-transfers

Requires scope pos:write

Record an in-store checkout and issue the buyer's digital passport.

A line carrying a unit_token is a serialised item, so the sale transfers real ownership: it requires buyer_email (an existing consumer account), writes the consumer_items row, and queues the custody, activate and certificate legs on chain. Sale and passport share one transaction — neither commits without the other.

409 if a referenced unit is not in stock at this shop (already sold, removed, or belonging to another shop).

Request body

  • source_terminalstring | null
  • destinationstring | null
  • line_itemsarray of objectsRequired

    What was sold.

    +Show 6 child attributes
    • unit_tokenstring | null

      The item's own token. It is what the item's QR code carries, and the id every item event uses.

    • product_namestringRequired
    • variant_labelstring | null
    • skustring | null

      Stock-keeping unit of the variant.

    • unit_price_eurnumberRequired

      Price of one unit in euro, including VAT.

    • quantityinteger

      Number of units.

      Defaults to 1

  • discount_kindstring | null
    One ofpercentamount
  • discount_valuenumber | null

    The rate/amount the worker entered — 0-100 when discount_kind is 'percent', a euro amount when 'amount'. Applied to the subtotal before VAT.

  • buyer_emailstring | null

    Buyer's account email — what the consumer reads out at the till. Resolved server-side to their internal user_id (stored as buyer_consumer_id on the transfer). Required when any line carries a unit_token — a serialised item cannot be sold into no account, or it leaves the shop as a passport orphan.

Returns

HTTP 201
  • transfer_idstringRequired
  • source_terminalstring | null
  • processed_attimestampRequired
  • statusstringRequired

    Current state of the object. See the allowed values.

    One ofcompletedprocessingcancelled
  • line_itemsarray of objects

    What was sold.

    +Show 8 child attributes
    • product_namestringRequired
    • variant_labelstring | null
    • skustring | null

      Stock-keeping unit of the variant.

    • unit_price_eurnumber

      Price of one unit in euro, including VAT.

      Defaults to 0

    • quantityinteger

      Number of units.

      Defaults to 1

    • subtotal_eurnumber

      Defaults to 0

    • unit_tokenstring | null

      The item's own token. It is what the item's QR code carries, and the id every item event uses.

    • consumer_item_idinteger | null

      The consumer_items row this sale produced — present only for serialised lines. Its absence on a line that carries a unit_token means the item left the shop without a digital passport.

  • subtotal_eurnumber

    Defaults to 0

  • discount_kindstring | null
    One ofpercentamount
  • discount_valuenumber | null
  • discount_eurnumber

    Defaults to 0

  • vat_rate_pctnumber

    Defaults to 0

  • vat_countrystring | null
  • vat_eurnumber

    Defaults to 0

  • total_eurnumber

    Total paid in euro, including VAT.

    Defaults to 0

  • buyer_consumer_idstring | null
  • storeobject | null

    Shop identity for receipts — sourced from retail_profiles + owner user.

    +Show 5 child attributes
    • store_namestring | null
    • citystring | null
    • countrystring | null
    • glnstring | null
    • emailstring | null
  • buyerobject | null

    Buyer identity when a consumer account was linked to the sale. No phone_number: consumers (users.role='Consumer') have no phone column anywhere in the schema — only email identifies them. Do not add a phone_number field here without first adding the column and a real collection point for it.

    +Show 3 child attributes
    • consumer_idstring | null
    • namestring | null
    • emailstring | null
POST /retail/pos-transfers
curl
curl -X POST http://62.169.23.239:8001/api/v1/retail/pos-transfers \
  -H "Authorization: Bearer $SKANNIT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "buyer_email": "customer@example.com",
    "source_terminal": "till-1",
    "line_items": [
      {
        "unit_token": "Qm7xK2pL9vR4tY8wZ1nB3c",
        "sku": "TT-100-WHT-M",
        "product_name": "Trail Tee",
        "variant_label": "White / M",
        "quantity": 1,
        "unit_price_eur": 29.0
      }
    ]
  }'
201Response
json
{
  "transfer_id": "2bc6a10e-b83d-56a1-b614-53e022ba4c86",
  "source_terminal": "till-1",
  "processed_at": "2026-10-09T09:30:00Z",
  "status": "completed",
  "line_items": [
    {
      "product_name": "Trail Tee",
      "variant_label": "White / M",
      "sku": "TT-100-WHT-M",
      "unit_price_eur": 29,
      "quantity": 1,
      "subtotal_eur": 29,
      "unit_token": "Qm7xK2pL9vR4tY8wZ1nB3c",
      "consumer_item_id": "7bf9462f-3324-593d-9012-c800e102b014"
    }
  ],
  "subtotal_eur": 29,
  "discount_kind": "percent",
  "discount_value": 24,
  "discount_eur": 0,
  "vat_rate_pct": 21,
  "vat_country": "NL",
  "vat_eur": 5.03,
  "total_eur": 29,
  "buyer_consumer_id": "b3a40984-b53d-50bf-b3de-f096ab2c40a8",
  "store": {
    "store_name": "Northwind Amsterdam",
    "city": "Rotterdam",
    "country": "NL",
    "gln": "8712345000004",
    "email": "ops@northwind.example"
  },
  "buyer": {
    "consumer_id": "f4811030-eecf-579d-a105-e02ee601e044",
    "name": "Northwind Apparel",
    "email": "ops@northwind.example"
  }
}

List till sales#

GET/retail/pos-transfers

Requires scope pos:read

Recent POS transfers / in-store checkout receipts for this shop.

Returns

HTTP 200
  • itemsarray of objects

    The objects on this page.

    +Show 6 child attributes
    • transfer_idstringRequired
    • transfer_typestringRequired
    • source_terminalstring | null
    • destinationstring | null
    • processed_attimestampRequired
    • statusstringRequired

      Current state of the object. See the allowed values.

      One ofcompletedprocessingcancelled
  • totalinteger

    Total count.

    Defaults to 0

GET /retail/pos-transfers
curl
curl http://62.169.23.239:8001/api/v1/retail/pos-transfers \
  -H "Authorization: Bearer $SKANNIT_TOKEN"
200Response
json
{
  "items": [
    {
      "transfer_id": "2bc6a10e-b83d-56a1-b614-53e022ba4c86",
      "transfer_type": "sale",
      "source_terminal": "till-1",
      "destination": "in_store",
      "processed_at": "2026-10-09T09:30:00Z",
      "status": "completed"
    }
  ],
  "total": 1
}

Retrieve a till sale#

GET/retail/pos-transfers/{transfer_id}

Requires scope pos:read

Single POS transfer detail — line items, VAT, totals.

Parameters

  • transfer_idstringpathRequired

Returns

HTTP 200
  • transfer_idstringRequired
  • source_terminalstring | null
  • processed_attimestampRequired
  • statusstringRequired

    Current state of the object. See the allowed values.

    One ofcompletedprocessingcancelled
  • line_itemsarray of objects

    What was sold.

    +Show 8 child attributes
    • product_namestringRequired
    • variant_labelstring | null
    • skustring | null

      Stock-keeping unit of the variant.

    • unit_price_eurnumber

      Price of one unit in euro, including VAT.

      Defaults to 0

    • quantityinteger

      Number of units.

      Defaults to 1

    • subtotal_eurnumber

      Defaults to 0

    • unit_tokenstring | null

      The item's own token. It is what the item's QR code carries, and the id every item event uses.

    • consumer_item_idinteger | null

      The consumer_items row this sale produced — present only for serialised lines. Its absence on a line that carries a unit_token means the item left the shop without a digital passport.

  • subtotal_eurnumber

    Defaults to 0

  • discount_kindstring | null
    One ofpercentamount
  • discount_valuenumber | null
  • discount_eurnumber

    Defaults to 0

  • vat_rate_pctnumber

    Defaults to 0

  • vat_countrystring | null
  • vat_eurnumber

    Defaults to 0

  • total_eurnumber

    Total paid in euro, including VAT.

    Defaults to 0

  • buyer_consumer_idstring | null
  • storeobject | null

    Shop identity for receipts — sourced from retail_profiles + owner user.

    +Show 5 child attributes
    • store_namestring | null
    • citystring | null
    • countrystring | null
    • glnstring | null
    • emailstring | null
  • buyerobject | null

    Buyer identity when a consumer account was linked to the sale. No phone_number: consumers (users.role='Consumer') have no phone column anywhere in the schema — only email identifies them. Do not add a phone_number field here without first adding the column and a real collection point for it.

    +Show 3 child attributes
    • consumer_idstring | null
    • namestring | null
    • emailstring | null
GET /retail/pos-transfers/{transfer_id}
curl
curl http://62.169.23.239:8001/api/v1/retail/pos-transfers/2bc6a10e-b83d-56a1-b614-53e022ba4c86 \
  -H "Authorization: Bearer $SKANNIT_TOKEN"
200Response
json
{
  "transfer_id": "2bc6a10e-b83d-56a1-b614-53e022ba4c86",
  "source_terminal": "till-1",
  "processed_at": "2026-10-09T09:30:00Z",
  "status": "completed",
  "line_items": [
    {
      "product_name": "Trail Tee",
      "variant_label": "White / M",
      "sku": "TT-100-WHT-M",
      "unit_price_eur": 29,
      "quantity": 1,
      "subtotal_eur": 29,
      "unit_token": "Qm7xK2pL9vR4tY8wZ1nB3c",
      "consumer_item_id": "7bf9462f-3324-593d-9012-c800e102b014"
    }
  ],
  "subtotal_eur": 29,
  "discount_kind": "percent",
  "discount_value": 24,
  "discount_eur": 0,
  "vat_rate_pct": 21,
  "vat_country": "NL",
  "vat_eur": 5.03,
  "total_eur": 29,
  "buyer_consumer_id": "b3a40984-b53d-50bf-b3de-f096ab2c40a8",
  "store": {
    "store_name": "Northwind Amsterdam",
    "city": "Rotterdam",
    "country": "NL",
    "gln": "8712345000004",
    "email": "ops@northwind.example"
  },
  "buyer": {
    "consumer_id": "f4811030-eecf-579d-a105-e02ee601e044",
    "name": "Northwind Apparel",
    "email": "ops@northwind.example"
  }
}