Migrating from Receipts to Orders

This guide is for integrations already sending sales data to 7shifts via /receipts and moving to
the new /orders API. If you're building a brand-new integration and have never used /receipts,
see the Orders guide instead — this document is a field-by-field
comparison, not a general introduction to the new endpoints.

This section focuses on:

  • Endpoint and method changes between /receipts and /orders
  • Which receipt fields map directly to order fields, and which don't carry over
  • Fields that are new in the Orders API and have no receipts equivalent
  • Structural differences (aggregate writes, reference data, reads, deletes)
  • A worked side-by-side example of the same sale sent to each API

Endpoint and method changes

ReceiptsOrders
POST /v2/company/{company_id}/receipts (create)PUT /api/v2/company/{company_id}/location/{location_id}/orders/{external_id} (create or update)
PUT /v2/company/{company_id}/receipts/ext:{location_id}:{receipt_id} (update)PUT /api/v2/company/{company_id}/location/{location_id}/orders/{external_id} (same call as create)
No delete endpoint (void via PUT + zeroed totals)DELETE /api/v2/company/{company_id}/location/{location_id}/orders/{external_id} (true soft-delete)
GET /v2/company/{company_id}/receipts/{receipt_id} (single) and GET /v2/company/{company_id}/receipts (list)GET /api/v2/company/{company_id}/location/{location_id}/orders/{external_id} (single) and GET .../orders (list)
No equivalent/revenue_centers, /sales_categories, /dining_options (PUT + GET single/list)

Notable differences:

  • location_id moves from the request body/URL-encoded external key into its own route segment
    (/location/{location_id}/orders/...).
  • The ext:{location_id}:{receipt_id} composite key in the receipts update URL is gone — the
    order route takes a plain {external_id}, with location_id as its own path segment.
  • Create and update are the same call on Orders (PUT, idempotent upsert on external_id +
    location_id + integration) — there's no separate POST for "first write".

Field mapping: Receipt → Order

Receipt fieldOrder fieldNotes
receipt_idexternal_id (route parameter)Same purpose (your own identifier), now part of the URL rather than the body.
location_idlocation_id (route parameter)Same purpose, now part of the URL rather than the body.
receipt_dateorder_dateSame meaning (timestamp the sale was placed); renamed.
net_totalnet_totalUnchanged.
gross_totalgross_totalUnchanged, but now required on Orders (optional/nullable on Receipts).
statusstatusSame enum values plus a new refund value: open closed voided deleted refund.
total_receipt_discountsdiscountsSame meaning; renamed, moved to order level.
tips(moved to payment level)No order-level tips field — tips are now recorded per payment, not summed on the parent.
external_user_idexternal_user_idUnchanged.
revenue_center (free-text label)revenue_center_external_id (reference)No longer a free-text string — references a RevenueCenter record you create via PUT /revenue_centers/{external_id}.
dining_option (free-text label)dining_option_external_id (reference)No longer a free-text string — references a DiningOption record you create via PUT /dining_options/{external_id}.
order_typedining_option_external_id (as a DiningOption.type)order_type (dine_in/delivery/take_out) is superseded by the type field on the referenced dining option.
receipt_lines[]line_items[]Same purpose (see line item mapping below); required (may be empty) instead of optional.
tip_details[]payments[]Tip line items are absorbed into the richer payments[] structure (see payment mapping below); required (may be empty) instead of optional.
receipt_close_date (read-only, response only)closed_dateSame meaning; renamed. On Orders this is a writable field, not read-only.
created_date (read-only, response only)created (read-only, response only)Same meaning; renamed.
modified_date (read-only, response only)modified (read-only, response only)Same meaning; renamed.
total_item_discounts (read-only, response only)(no direct equivalent)Not surfaced as a distinct order-level field — sum each line item's own discounts if you need this total.

Field mapping: Receipt line → Order line item

Receipt line fieldLine item fieldNotes
external_item_id(no equivalent)Line items don't carry an item identifier; only external_id (the line item's own identifier) is available, and it's optional.
external_category_ids[]sales_category_external_ids[]Same purpose; now references SalesCategory records you create via PUT /sales_categories/{external_id}, rather than arbitrary category ID strings.
quantityquantityUnchanged in purpose; type changes from integer to float (fractional quantities are now supported).
price(no direct equivalent)Not carried over as a distinct field — use gross_total/net_total on the line item.
gross_item_pricegross_totalSame meaning; renamed.
net_item_pricenet_totalSame meaning; renamed.
item_discountdiscountsSame meaning; renamed.
statusstatusSame enum values plus a new refund value.
createdexternal_createdSame meaning; renamed for consistency with the new external_created/created pair (see below).
(no equivalent)is_revenue_itemNew — see New fields.
(no equivalent)feesNew — see New fields.

Field mapping: Tip detail → Payment

Tip details on a receipt were a flat list of { type, value } pairs with no concept of an
individual payment. Orders replace this with a full payments[] array — one entry per actual
payment, each of which may carry its own tip:

Tip detail fieldPayment fieldNotes
valuetipsA tip detail's value becomes a payment's tips, but now scoped to the specific payment it belongs to rather than summed across the whole receipt.
type: 'gratuity'gratuitiesGratuity amounts get their own dedicated field on the payment rather than being one of several type values in a flat list.
type: 'cc' / 'cash' / 'declared' / 'net' / 'total'payment_type / payment_source_typeThese distinctions move to payment_type (credit, cash, other, unknown_source) and payment_source_type (a free-text descriptor, e.g. visa) on the payment, rather than being encoded in the tip's type.
(no equivalent)totalNew, and required — the payment's own total, not just its tip. Receipts never modeled payments as first-class objects, only their tips.
(no equivalent)fees, discounts, status, external_user_idNew — see below.

New fields with no receipts equivalent

These fields did not exist anywhere in the receipts contract:

FieldWhereDescription
external_business_dateOrderPOS-provided business date for daily sales roll-ups that can span past midnight, distinct from the wall-clock order_date.
taxOrderTax total, broken out on its own (receipts folded tax into net_total/gross_total only).
feesOrder, Payment, Line ItemFee amounts, broken out at each level.
non_revenue_totalOrderNon-revenue total on the order.
item_countOrderNumber of items on the order.
is_revenue_itemLine ItemWhether the line item contributes to revenue.
payment_type, payment_source_type, statusPaymentPayment classification and its own status, independent of the order/line item status. Receipts had no concept of an individual payment at all, only summed tips.
external_created / external_modifiedOrder, Payment, Line ItemYour own system's creation/modification timestamps, preserved round-trip. Distinct from the renamed created_date/modified_date (see field mapping above), which are 7shifts-assigned.
warnings[]Order (read-only, response only)Surfaces any revenue_center_external_id / dining_option_external_id / sales_category_external_ids that didn't resolve on write and were backfilled with a placeholder.
nameRevenue Center, Sales Category, Dining OptionReference data has no receipts equivalent at all — these are new resources.
typeDining OptionPartner's classification of the dining option (e.g. dine_in, delivery, take_out), replacing the receipt's order_type.

Structural differences

These are behavioral differences that go beyond a simple field rename — they change how you should
build your integration, not just what to call a field.

  • Full-replacement writes. A receipt's receipt_lines/tip_details were just nested arrays on
    an otherwise flat object, with no strong statement about what a partial update did to them. An
    order is a true aggregate root: payments[] and line_items[] are required on every PUT
    (they may be empty, but must be present), and each PUT replaces the previously stored
    payments/line items entirely — there's no partial merge. Always send the full current set of
    children, not just what changed since the last write.
  • Unresolved references don't fail the write. If you reference a revenue center, dining
    option, or sales category external_id that hasn't been created yet, 7shifts creates a
    placeholder record automatically and returns a warnings[] entry describing it, rather than
    rejecting the order. Pushing the real reference data later with the same external_id corrects
    the placeholder in place. Receipts had no equivalent concept, since revenue_center/
    dining_option were free-text and never needed to resolve against anything.
  • Delete cascades to children. DELETE /orders/{external_id} soft-deletes the order and
    cascades that same soft-delete to its payments and line items in one call — idempotent, and
    404 Not Found if the order never existed. Receipts had no delete at all, only the PUT-based
    voiding pattern (which still works the same way on Orders).

The receipts-to-orders transition date

Each location's integration tracks a single cutover point in time (the "transition date") that
determines whether a given point in its sales history is served from the old receipts data or the
new orders data. This isn't something you set directly through the public API — it's established
automatically:

  • If a location has never received an order before, the first PUT /orders/{external_id}
    request sent for it sets the transition date to that order's order_date. This is write-once:
    once set, it's never moved automatically by a later write.
  • Orders dated on or after the transition date are served from the new orders data; anything
    dated before it is still served from whatever receipts data already exists for that location.
  • Because of the write-once behavior, send your oldest order first when you start integrating
    a location (including historical backfill). If you send a recent order first, the transition
    date gets set to that later timestamp, and any earlier orders you send afterward fall before the
    cutover and won't be visible through the Orders read endpoints.
  • If a location's transition date needs to be corrected after the fact (e.g. it was seeded by the
    wrong order), that's a manual, internal-only correction — reach out to [email protected]
    rather than trying to fix it by re-sending orders.

Side-by-side example

The same sale — a $20.00 food item, a $10.00 drink item, $3.99 tax, and a card payment (with a
$9.00 tip) — sent to each API. Net total: $30.00. Gross total: $33.99.

Receipts

curl --request POST --url 'https://api.7shifts.com/v2/company/12345/receipts'
{
  "receipt_id": "rec_98765",
  "location_id": 7890,
  "receipt_date": "2023-12-31T21:45:23Z",
  "net_total": 3000,
  "gross_total": 3399,
  "total_receipt_discounts": 0,
  "tips": 900,
  "status": "closed",
  "revenue_center": "Bar",
  "dining_option": "Dine In",
  "order_type": "dine_in",
  "receipt_lines": [
    {
      "external_item_id": "item-1",
      "external_category_ids": ["food"],
      "quantity": 1,
      "price": 2000,
      "gross_item_price": 2000,
      "net_item_price": 2000,
      "item_discount": 0,
      "status": "closed"
    },
    {
      "external_item_id": "item-2",
      "external_category_ids": ["drink"],
      "quantity": 1,
      "price": 1000,
      "gross_item_price": 1000,
      "net_item_price": 1000,
      "item_discount": 0,
      "status": "closed"
    }
  ],
  "tip_details": [
    { "type": "cc", "value": 900 }
  ]
}

Orders

Revenue center, sales categories, and dining option are created once, ahead of time:

curl --request PUT --url 'https://api.7shifts.com/api/v2/company/12345/location/7890/revenue_centers/bar'
curl --request PUT --url 'https://api.7shifts.com/api/v2/company/12345/location/7890/sales_categories/food'
curl --request PUT --url 'https://api.7shifts.com/api/v2/company/12345/location/7890/sales_categories/drink'
curl --request PUT --url 'https://api.7shifts.com/api/v2/company/12345/location/7890/dining_options/dine-in'

Then the order itself:

curl --request PUT --url 'https://api.7shifts.com/api/v2/company/12345/location/7890/orders/order-98765'
{
  "revenue_center_external_id": "bar",
  "dining_option_external_id": "dine-in",
  "order_date": "2023-12-31T21:45:23Z",
  "net_total": 3000,
  "gross_total": 3399,
  "tax": 399,
  "discounts": 0,
  "status": "closed",
  "payments": [
    {
      "total": 3399,
      "tips": 900,
      "payment_type": "credit",
      "payment_source_type": "visa",
      "status": "closed"
    }
  ],
  "line_items": [
    {
      "sales_category_external_ids": ["food"],
      "quantity": 1,
      "net_total": 2000,
      "gross_total": 2000,
      "is_revenue_item": true,
      "status": "closed"
    },
    {
      "sales_category_external_ids": ["drink"],
      "quantity": 1,
      "net_total": 1000,
      "gross_total": 1000,
      "is_revenue_item": true,
      "status": "closed"
    }
  ]
}

Rollout checklist

  1. Start sending revenue centers, sales categories, and dining options for each location ahead of
    its orders.
  2. Switch new/updated sales writes from /receipts to PUT /orders/{external_id}, mapping fields
    per the tables above. Send your oldest order for a location first, so the transition date (see
    above) lands where you expect.
  3. Stop sending /receipts for a location once its Orders integration is verified — the two
    should not both be actively written for the same location.
  4. Update any downstream logic that read receipts totals to instead call GET /orders /
    GET /orders/{external_id}, if applicable.

Did this page help you?