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
/receiptsand/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
| Receipts | Orders |
|---|---|
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_idmoves 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}, withlocation_idas its own path segment. - Create and update are the same call on Orders (
PUT, idempotent upsert onexternal_id+
location_id+ integration) — there's no separatePOSTfor "first write".
Field mapping: Receipt → Order
| Receipt field | Order field | Notes |
|---|---|---|
receipt_id | external_id (route parameter) | Same purpose (your own identifier), now part of the URL rather than the body. |
location_id | location_id (route parameter) | Same purpose, now part of the URL rather than the body. |
receipt_date | order_date | Same meaning (timestamp the sale was placed); renamed. |
net_total | net_total | Unchanged. |
gross_total | gross_total | Unchanged, but now required on Orders (optional/nullable on Receipts). |
status | status | Same enum values plus a new refund value: open closed voided deleted refund. |
total_receipt_discounts | discounts | Same 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_id | external_user_id | Unchanged. |
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_type | dining_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_date | Same 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 field | Line item field | Notes |
|---|---|---|
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. |
quantity | quantity | Unchanged 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_price | gross_total | Same meaning; renamed. |
net_item_price | net_total | Same meaning; renamed. |
item_discount | discounts | Same meaning; renamed. |
status | status | Same enum values plus a new refund value. |
created | external_created | Same meaning; renamed for consistency with the new external_created/created pair (see below). |
| (no equivalent) | is_revenue_item | New — see New fields. |
| (no equivalent) | fees | New — 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 field | Payment field | Notes |
|---|---|---|
value | tips | A 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' | gratuities | Gratuity 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_type | These 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) | total | New, 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_id | New — see below. |
New fields with no receipts equivalent
These fields did not exist anywhere in the receipts contract:
| Field | Where | Description |
|---|---|---|
external_business_date | Order | POS-provided business date for daily sales roll-ups that can span past midnight, distinct from the wall-clock order_date. |
tax | Order | Tax total, broken out on its own (receipts folded tax into net_total/gross_total only). |
fees | Order, Payment, Line Item | Fee amounts, broken out at each level. |
non_revenue_total | Order | Non-revenue total on the order. |
item_count | Order | Number of items on the order. |
is_revenue_item | Line Item | Whether the line item contributes to revenue. |
payment_type, payment_source_type, status | Payment | Payment 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_modified | Order, Payment, Line Item | Your 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. |
name | Revenue Center, Sales Category, Dining Option | Reference data has no receipts equivalent at all — these are new resources. |
type | Dining Option | Partner'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_detailswere 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[]andline_items[]are required on everyPUT
(they may be empty, but must be present), and eachPUTreplaces 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 categoryexternal_idthat hasn't been created yet, 7shifts creates a
placeholder record automatically and returns awarnings[]entry describing it, rather than
rejecting the order. Pushing the real reference data later with the sameexternal_idcorrects
the placeholder in place. Receipts had no equivalent concept, sincerevenue_center/
dining_optionwere 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 Foundif the order never existed. Receipts had no delete at all, only thePUT-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'sorder_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
- Start sending revenue centers, sales categories, and dining options for each location ahead of
its orders. - Switch new/updated sales writes from
/receiptstoPUT /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. - Stop sending
/receiptsfor a location once its Orders integration is verified — the two
should not both be actively written for the same location. - Update any downstream logic that read receipts totals to instead call
GET /orders/
GET /orders/{external_id}, if applicable.
Updated 8 days ago
