Order mapping
Orders are the heaviest resource. StagingOrder mixes flat fields
(addresses split into shipping_address1 etc., a single discount
shape) with nested pass-through arrays for line items,
transactions, tax lines, and shipping lines.
Quick reference — minimum loadable order
{
"original_id": increment_id,
"name": "#" & $string(increment_id),
"email": customer_email,
"processedAt": created_at,
"currency": order_currency,
"financialStatus": "PAID",
"lineItems": items.{
"title": name,
"sku": sku,
"quantity": qty_ordered,
"priceSet": {
"shopMoney": { "amount": $string(price), "currencyCode": $$.order_currency }
}
}
}processedAt is critical. If you omit it, the order’s
“created at” defaults to load time, breaking historical reports.
Always emit it from the source’s order timestamp.
Identity & status
namestringOrder display name (e.g. #1042). Customers and merchants see this.
"name": "#" & $string(increment_id)emailstringOrder contact email — used for notifications. Falls back to the customer’s email if omitted.
phonestringOrder contact phone, and the phone on the customer the order upserts.
Shopify only accepts E.164 (+15555550123); a local number is
converted for you using billing_country (falling back to
shipping_country). Placeholders that hold no number (0, -,
blank) are treated as no phone; anything else that can’t be resolved
fails validation with INVALID_FORMAT rather than at load time.
customerIdstringThe customer’s id in the source store. Graftport swaps it for the migrated customer, attaching the order to a customer record that already exists.
"customerId": customer_idPrefer this over the email / firstName / lastName fields wherever
the source knows which customer placed the order. Without it, loading
an order writes a customer built from whatever the order carries,
which overwrites fields the order doesn’t know about. Leave it unmapped
for guest checkouts — the order then arrives with no customer attached,
which is what a guest checkout is.
Migrate customers before orders, which is the order Graftport runs them in by default. If an order loads before the customer it points at, the order still arrives complete — with its own lines, totals and addresses — but with no customer attached, and re-running won’t attach one later: Shopify lets you change an existing order’s note, tags and shipping address, not who placed it. Include customers in the same migration as orders and this resolves on its own.
discountCodeTextstringThe discount code the customer actually typed. Without it, an order that carries a discount amount gets a generated placeholder code, which is what shows on the order in the admin.
"discountCodeText": coupon_codeprocessedAtstring (ISO8601)When the order was placed on the source platform. Always emit this on historical orders so the timeline is preserved.
"processedAt": created_atclosedAtstring (ISO8601)When the order was closed (typically equal to processedAt for
fully-fulfilled historical orders).
financialStatusstringOne of PENDING, AUTHORIZED, PARTIALLY_PAID, PAID,
PARTIALLY_REFUNDED, REFUNDED, VOIDED.
"financialStatus": is_paid ? "PAID" : "PENDING"fulfillmentStatusstringOne of FULFILLED, PARTIALLY_FULFILLED, RESTOCKED. Omit for
unfulfilled orders.
taxesIncludedbooleantrue if line item prices already include tax (common in EU
merchants), false if tax is added on top (common in US).
buyerAcceptsMarketingbooleansourceIdentifierstringThe original platform’s order ID — preserves cross-system
traceability. Often the same as original_id for orders.
sourceNamestringWhere the order originated (e.g. web, pos, magento).
Currency
currencystringISO 4217 currency code for the shop’s base currency on this order.
Used inside priceSet.shopMoney.currencyCode of every line item.
"currency": order_currencypresentmentCurrencystringThe currency the customer saw at checkout, if multi-currency. Often
equal to currency for single-currency orders.
Customer
The order's customer is composed from these fields. The staging model assembles them into Shopify's customer.toUpsert shape.
firstNamestringUsed on the order’s customer record AND on shipping/billing addresses.
lastNamestringcustomerNotestringNote attached to the customer on this order. Different from note
(below), which is the order-level admin note.
customerTagsstring[]Tags attached to the customer (not the order).
Shipping address (flat)
Shopify nests its address shapes; the staging model flattens them. Setting shipping_address1 is what triggers the shipping address being attached.
shipping_address1string"shipping_address1": shipping.streetshipping_address2stringshipping_citystringshipping_provincestringState / province.
shipping_countrystringCountry code (ISO 3166-1 alpha-2 preferred).
shipping_zipstringshipping_firstNamestringRecipient’s first name. A gift often ships to someone other than the account holder, so map this when the source keeps a separate delivery name. Left unmapped, the customer’s own name is used.
shipping_lastNamestringshipping_phonestringContact number for the delivery, used by carriers. Normalized against
shipping_country.
Billing address (flat)
Same shape as shipping, with a billing_ prefix. Setting billing_address1 triggers the billing address.
billing_address1stringbilling_address2stringbilling_citystringbilling_provincestringbilling_countrystringbilling_zipstringbilling_firstNamestringName the invoice is addressed to — accounts payable, rather than the person who placed the order. Falls back to the customer’s name.
billing_lastNamestringbilling_phonestringLine items (nested pass-through)
Each line item is a Shopify-shaped object. The staging model validates each one and rejects malformed entries early.
lineItemsLineItem[]requiredArray of order line items. Each entry has the same shape Shopify’s
OrderCreateLineItemInput accepts:
| Field | Notes |
|---|---|
title | Product name as it was at purchase. |
quantity | int. Required per line. |
sku | Variant SKU at purchase. |
variantId | The variant’s id in the source store. Graftport swaps it for the migrated variant once the products have loaded. This is what connects the order to the catalogue — without it the line is just a title and a price. |
productId | Same, for the product the variant belongs to. |
priceSet | { shopMoney: { amount: string, currencyCode } } — money is always strings. |
taxLines | Per-line-item tax breakdown: { title, rate, priceSet }. |
Anything else on a line item is rejected — map it as an order custom attribute instead.
A line whose variantId or productId names something that hasn’t
migrated yet does not hold the order back: the order is created
right away with the line kept — title, quantity and price intact —
but without the link into your catalogue, and re-running won’t
attach the link later because Shopify doesn’t allow an existing
order’s lines to be rewired. If you want variant-linked line items,
migrate products before orders.
"lineItems": items.{
"title": name,
"sku": sku,
"quantity": qty_ordered,
"priceSet": {
"shopMoney": {
"amount": $string(price),
"currencyCode": $$.order_currency
}
}
}Discounts (flat — pick one)
Set exactly one discount field. The staging model wires it into Shopify's discount-code shape with a placeholder code.
itemPercentageDiscountnumberPercentage off as a number 0-100 (25 = 25%). Applied to qualifying
items.
"itemPercentageDiscount": discount_percentitemFixedDiscountnumberFixed money amount off (in the order’s currency). Rounded to 2 decimals.
freeShippingDiscountbooleantrue if the order had a free-shipping discount.
Fulfillment (flat)
Setting locationId is what triggers the fulfillment record. Without locationId, no fulfillment is created — even if you set the other fields.
locationIdstringThe Shopify location GID this order ships from. Required to create a fulfillment.
"locationId": $$.config.default_location_gidnotifyCustomerbooleanWhether to send a fulfillment notification email when the load creates the fulfillment. Default behavior is Shopify’s (no email for migrated orders is usually right).
trackingCompanystringCarrier name (e.g. UPS, DHL).
trackingNumberstringshipmentStatusstringOne of Shopify’s shipment statuses (e.g. DELIVERED,
IN_TRANSIT).
Transactions (nested pass-through)
transactionsTransaction[]Array of payment transactions. Each entry follows Shopify’s
OrderCreateOrderTransactionInput shape:
| Field | Notes |
|---|---|
kind | SALE, AUTHORIZATION, CAPTURE, REFUND, VOID, CHANGE. |
status | SUCCESS, FAILURE, PENDING, ERROR. |
amountSet | Money bag — { shopMoney: { amount, currencyCode } }. |
gateway | Payment processor name. |
processedAt | ISO8601. |
"transactions": [{
"kind": "SALE",
"status": "SUCCESS",
"gateway": payment_method,
"processedAt": created_at,
"amountSet": {
"shopMoney": { "amount": $string(grand_total), "currencyCode": order_currency }
}
}]Tax & shipping lines (nested pass-through)
taxLinesTaxLine[]Order-level tax breakdown. Each entry is { title, rate, priceSet }.
Order-level tax is only honoured when no line item carries its own, so
pick one level and stay there — mixing the two loses tax. If your
source breaks tax down per line, put it on the lines (and on the
shipping lines) and leave this field out.
shippingLinesShippingLine[]Shipping methods used on the order. Each entry is { title, code, source, priceSet, taxLines }.
code is the carrier’s rate code and source the rate provider, both
shown on the order in the admin. taxLines is the tax charged on the
delivery itself — it belongs here rather than on the order whenever the
line items carry their own tax.
"shippingLines": [{
"title": shipping_method,
"code": shipping_code,
"priceSet": {
"shopMoney": { "amount": $string(shipping_amount), "currencyCode": order_currency }
},
"taxLines": [{
"title": "VAT",
"rate": tax_rate,
"priceSet": {
"shopMoney": { "amount": $string(shipping_tax), "currencyCode": order_currency }
}
}]
}]Custom attributes & notes
dict_customAttributesobjectFree-form { key, value } map of order-level custom attributes
(notes from checkout, gift options, custom forms). Each entry
becomes one Shopify custom attribute.
"dict_customAttributes": {
"gift_message": gift_message,
"preferred_delivery": delivery_window
}notestringOrder-level admin note (separate from customerNote).
tagsstring[] | stringOrder-level tags.
Patterns
Magento order — minimum viable
{
"original_id": increment_id,
"name": "#" & increment_id,
"email": customer_email,
"phone": billing_address.telephone,
"firstName": customer_firstname,
"lastName": customer_lastname,
"processedAt": created_at,
"currency": order_currency_code,
"presentmentCurrency": order_currency_code,
"financialStatus": "PAID",
"taxesIncluded": false,
"shipping_address1": shipping_address.street[0],
"shipping_city": shipping_address.city,
"shipping_province": shipping_address.region_code,
"shipping_country": shipping_address.country_id,
"shipping_zip": shipping_address.postcode,
"billing_address1": billing_address.street[0],
"billing_city": billing_address.city,
"billing_country": billing_address.country_id,
"billing_zip": billing_address.postcode,
"lineItems": items.{
"title": name,
"sku": sku,
"quantity": qty_ordered,
"priceSet": {
"shopMoney": { "amount": $string(price), "currencyCode": $$.order_currency_code }
}
},
"transactions": [{
"kind": "SALE",
"status": "SUCCESS",
"gateway": payment.method,
"processedAt": created_at,
"amountSet": {
"shopMoney": { "amount": $string(grand_total), "currencyCode": order_currency_code }
}
}]
}Order with a percentage discount
{
"original_id": increment_id,
…,
"itemPercentageDiscount": discount_percent
}The staging model wires this into a Shopify discount code with a
placeholder code (migration_placeholder_percentage_code) — the
discount applies to the order, but no real coupon code is created.
Order with shipping fulfillment
{
"original_id": increment_id,
…,
"locationId": $$.config.default_location_gid,
"trackingCompany": shipment.carrier,
"trackingNumber": shipment.tracking_number,
"shipmentStatus": shipment.status = "delivered" ? "DELIVERED" : "IN_TRANSIT"
}Capture custom checkout fields
"dict_customAttributes": {
"gift_message": gift_message,
"delivery_instructions": delivery_notes,
"purchase_order": po_number
}Re-loading an order that already exists
Fix a mapping, re-run the load, and the order on Shopify matches — line items included. Getting there takes two different routes, because Shopify only lets a small part of an existing order change in place.
Changed in place (fast, non-destructive):
| Field | Mapping fields |
|---|---|
email | |
| Phone | phone |
| Shipping address | shipping_address1, shipping_city, shipping_zip, … |
| Note | note |
| Tags | tags |
| Custom attributes | dict_customAttributes |
| Metafields | metafields |
Everything else rebuilds the order. Line items and quantities, prices and totals, tax lines, shipping lines, discounts, transactions, payment and fulfillment status, currency, processed date and the billing address cannot be altered on an order that already exists — so when any of them changes, Graftport rebuilds the order: it creates the corrected order first, then removes the original. If removing the original ever fails, the run still succeeds and the record carries a warning naming the leftover duplicate so you can delete it in the Shopify admin.
A rebuild only happens when one of those fields actually changed. Editing a tag or a note never rebuilds anything. On a same-store migration the comparison is against what your store holds right now, so the very first run already rebuilds exactly the orders your mapping changes — and leaves every other order untouched.
What survives a rebuild: the order number (name), the order date
(processedAt), and every field your mapping produces — the new order is
built from the same payload a first-time load would use.
What does not: anything added to the order inside Shopify after it was migrated — manual refunds, staff notes, fulfillment changes. The order’s “New order” staff notification also fires again.
Run a dry run first. A dry run reports how many orders would be rebuilt, per resource, without touching Shopify. On a large re-run that number is the thing to sanity-check before you commit.
To keep re-runs strictly non-destructive, use Update specific fields only in the run options and pick from the in-place list above. Orders are then never rebuilt — but line-item and money corrections won’t apply either, and the record says so with a warning naming what it couldn’t change.
Gotchas
Money amounts are strings inside priceSet. amount: 19.99
fails; amount: "19.99" works. Wrap with $string().
processedAtis critical for historical orders. Without it, the order shows up as created today.- Variants must exist before orders. Run the products load
before the orders load — otherwise
lineItemscan’t resolve to Shopify variant IDs and lose their product link. - Discount codes are placeholders. Setting
itemPercentageDiscountdoesn’t create a real reusable code on Shopify — it applies the discount to this single order with a generated placeholder. Use the discount code resource for real reusable codes. - Tax lines at order level vs line-item level: don’t mix. Pick one consistent level per merchant.
- Currency consistency. Top-level
currencyand thecurrencyCodeinside everypriceSet.shopMoneymust match for single-currency orders. shipping_address1is the trigger — without it, no shipping address is created even if you set city/zip/country.- Fulfillment requires
locationId. SettingtrackingNumberalone produces no fulfillment record. - Disable Shopify staff order notifications first. Every loaded order triggers Shopify’s “New order” staff email, which floods inboxes on a historical migration. Turn them off before the load — see Shopify destination → Before migrating orders.