# Shopify orders

> Sayproof reading orders from Shopify itself; app details, setup, status mapping, the delivery rule and showing reviews on the product page.

Source: /en/integrate/shopify/

On a store that runs on Shopify, no code needs to be written to get orders to Sayproof. Sayproof connects to the Shopify Admin API with the details of a Shopify app you install on your store and reads new or changed orders itself at regular intervals (every 15 minutes by default). The orders read are recorded in the same way as orders [sent by your store system](/en/integrate/orders.md); the right to write a review and the invitations follow the same rules.

The Shopify screen names on this page are those of the English admin; if your Shopify admin is in another language, the same places appear under their translated names.

## When to use it

- If your store runs on Shopify, set up the connection on this page.
- If your system has no ready connection, your store system sends the orders: [Sending orders and deliveries](/en/integrate/orders.md).

In the panel the two ways are shown one under the other on the **Integration › Order transfer** tab: **Read from your platform** and **Sent by your store**.

## What you need

| Item | Description |
|---|---|
| Shopify store address | Your store's `myshopify.com` address, for example `yourbrand.myshopify.com`. It is shown in the Shopify admin under **Settings › Domains**. You do not need to write `https://`; if you do, only the domain is used. Your own domain (`yourbrand.com`) and the `admin.shopify.com` address are not accepted. |
| Client ID and Client secret | The details of the app you create for this job in the Shopify Dev Dashboard and install on your store. The app only needs the `read_orders` scope. |
| Sayproof channel | The channel the orders read are written to. The order's market is this channel's market. |

- By default Shopify gives apps the orders of the last 60 days. Even if **How many days to read back the first time** is set above 60, older orders do not arrive; for those the app also needs the `read_all_orders` scope, which depends on Shopify's approval.
- The customer's email address is protected customer data in Shopify. Apps created in the Dev Dashboard can access it; for custom apps created the old way in the store admin, access depends on the store's Shopify plan. If the **Has email** column shows **No** in the try, the app cannot read the email and no invitation can be sent for those orders. The order's phone number is protected customer data too; if the app cannot read it, the field arrives empty and the read does not stop.

A Sayproof channel can have one connection per platform.

## Creating the app in Shopify

New custom apps can no longer be created in the Shopify admin; the app is created in the Shopify Dev Dashboard:

1. In the Dev Dashboard, under **Apps**, press **Create app**, choose **Start from Dev Dashboard** and give the app a name (for example "Sayproof").
2. On the app's **Versions** page, write `read_orders` in the **Scopes** field and publish the version with **Release**.
3. Open the app and install it on your store with the **Install app** button on the **Installs** card.
4. Copy the **Client ID** and **Client secret** values on the **App settings** page.

The app and the store must belong to the same organisation in Shopify; Sayproof asks your store for a token with these details. If a scope is added later, a new version is released and the change is approved on the store.

If you already have a custom app created earlier in the store admin (**Apps › Develop apps**), you can also use its **Admin API access token** with the `read_orders` scope: leave the **Client ID** field empty and write the token in the **Client secret** field.

## Setting up in the panel

1. On the **Integration › Order transfer** tab, press **Set up a connection** in the **Read from your platform** section.
2. In the **Where to read from** step, choose **Shopify** as the platform and the Sayproof channel.
3. In the **Connection details** step, fill in **Shopify store address**, **Client ID** and **Client secret**. **How many days to read back the first time** takes a value between 1 and 365; the default is 30 days.
4. In the **Try it** step, press **Try the connection**. Sayproof connects to Shopify and reads the first page (at most 25 orders) of the orders within the chosen number of days. The result shows the number of orders read, the orders that could not be read and at most five sample orders: **Order no.**, **Order date**, **Payment**, **Products**, **Shipping status**, **Has email**. The customer's email address is not shown. Nothing is saved in this step.
5. **Save and start reading** only becomes available after a successful try. If any detail changes after the try, the connection has to be tried again.

After saving, the connection appears on the **Platform connections** card. The card shows **Read frequency**, **Last read**, **In the last read** (orders read and how many were new), **Could not be read** and **Last error**. Buttons:

| Button | Purpose |
|---|---|
| **Read now** | Starts a read without waiting for the next one. It is unavailable while the connection is paused or the channel is off. |
| **Try the connection** | Repeats the try with the saved details; saves nothing. |
| **Pause** / **Resume** | Stops reading or starts it again. |
| **Edit the connection** | Changes the **Shopify store address** and the **Read frequency** (between 5 and 1440 minutes). Reading continues where it left off; orders already transferred are not read again. If the address points to another store, the **Client ID** and **Client secret** are entered again in the same form; the saved details are not sent to the new address (`400 invalid / address_needs_credentials`). The Sayproof channel does not change in this form. |
| **Change the sign-in details** | Saves the new **Client secret**; if the **Client ID** is left empty, the current value is kept. |
| **Remove the connection** | Stops reading. Orders already read and invitations already sent are not deleted. |

## Reading

- A read runs every 15 minutes by default; the frequency can be changed to between 5 and 1440 minutes with **Edit the connection**. The first read takes the orders whose change time falls within the chosen number of days.
- Later reads take the orders changed since 10 minutes before the previous read started. Reading the same order again is safe: the order is updated and no duplicate is created.
- Only one read runs at a time for a connection.
- A read takes at most 50 pages (at most 25 orders per page). If Shopify finds a page's query cost too high, the page is asked for again with fewer orders. If the limit is reached before the orders run out, the card shows "The last read hit the page limit" and the next read continues from the change time of the last order read (with a 10-minute margin).
- If Shopify reports that the request limit has been reached, the read stops at that page and the next read continues where it left off. If this happens on the first page, the read counts as failed.
- If a read fails, its position does not move; the next read tries the same range again after the read interval and the reason appears on the **Last error** line.

If **Client ID** is filled in, Sayproof gets a token from your store with the app details at the start of each read; if Client ID is empty, the old custom app token in the Client secret field is used directly. The token is valid for 24 hours and is not stored by Sayproof.

```http
POST https://yourbrand.myshopify.com/admin/oauth/access_token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=<client id>&client_secret=<client secret>
```

Orders are read from version `2026-10` of the GraphQL Admin API, sorted by change time:

```http
POST https://yourbrand.myshopify.com/admin/api/2026-10/graphql.json
Content-Type: application/json
X-Shopify-Access-Token: <token>

{"query": "query SayproofOrders($first: Int!, $after: String, $query: String!) { orders(first: $first, after: $after, query: $query, sortKey: UPDATED_AT) { … } }", "variables": {"first": 25, "after": null, "query": "updated_at:>='2026-09-02T12:00:00Z'"}}
```

- Further pages are reached with the `pageInfo.endCursor` value of the answer.
- If the token is not accepted (`401`) and Client ID is filled in, Sayproof gets a new token once; if an old custom app token is not accepted, the read stops with `api_credentials_refused`.
- Each request times out after 30 seconds and an answer may be at most 20 MB.

## Fields read

| Information in Sayproof | Field read in Shopify |
|---|---|
| Order number | `name` (for example `#1001`) |
| Payment status | `displayFinancialStatus` |
| Order date | `createdAt` |
| Change time | `updatedAt` |
| Cancellation | `cancelledAt` |
| Customer email | `email` |
| Customer phone | `phone` |
| Language | `customerLocale` (first part only; `tr-TR` → `tr`) |
| Product lines | `lineItems`; the order's first 50 lines |
| Line identifier | The number at the end of the line's `id` |
| Product code | The line's `sku`; the variant's `legacyResourceId` when empty |
| Group code | The product's `legacyResourceId` (`product.id` in Liquid) |
| Product name | The line's `name` |
| Quantity | `currentQuantity`; `quantity` when it is zero |
| Line status | The `displayStatus` of the shipments the line is in (`fulfillments`, the order's first 5 shipments); see [Delivery rule](#delivery-rule) |

- The customer email does the same job as the `customer_email` field in the order data sent by a store system: the invitation is sent to this address and the order is verified with it.
- The customer phone does the same job as the `customer_phone` field in the order data: the number is for [invitations on WhatsApp](/en/features/invitations.md#invitations-on-whatsapp). Until invitations by WhatsApp arrive with v2, the phone number is not stored.
- The product code must match the product key in your product source and the code given to the container on the product page.
- The customer record is not read, so the app does not need the `read_customers` scope. The order's address, price and payment details are not stored in Sayproof.

## Status mapping

The payment status comes from the order's `displayFinancialStatus`:

| Value in Shopify | Meaning | Payment |
|---|---|---|
| `PENDING` | Payment pending | Payment pending |
| `AUTHORIZED` | Payment authorised, not captured yet | Payment pending |
| `PARTIALLY_PAID` | Partially paid | Payment pending |
| `PAID` | Paid | Paid |
| `PARTIALLY_REFUNDED` | Partially refunded | Partially refunded |
| `REFUNDED` | Refunded | Refunded |
| `VOIDED` | Authorisation voided | Voided |
| `EXPIRED` | Authorisation expired | Voided |

Each line's shipping status comes from the `displayStatus` of the shipment the line is in, or from the Sayproof codes below:

| Code | When | Shipping status |
|---|---|---|
| `UNFULFILLED` | The line is not in a shipment yet | Placed |
| `SUBMITTED`, `LABEL_PURCHASED`, `LABEL_PRINTED`, `LABEL_VOIDED` | The shipment is being prepared | Preparing |
| `FULFILLED`, `MARKED_AS_FULFILLED`, `CONFIRMED`, `FAILURE` | The shipment was created; a shipment with an empty `displayStatus` also counts as `FULFILLED` | Shipped |
| `CARRIER_PICKED_UP`, `IN_TRANSIT`, `OUT_FOR_DELIVERY`, `DELAYED`, `ATTEMPTED_DELIVERY`, `NOT_DELIVERED`, `READY_FOR_PICKUP` | The shipment is on its way or ready for pickup | In transit |
| `DELIVERED`, `PICKED_UP` | The shipment was delivered or the customer picked it up | Delivered |
| `CANCELLED` | The order was cancelled (`cancelledAt` is set) | Cancelled |
| `REMOVED` | The whole line was refunded or removed from the order before it was shipped | Cancelled |
| `REFUNDED` | The whole line was refunded after it was in a shipment | Returned |

- Cancelled, errored or failed shipments (`status` of `CANCELLED`, `ERROR` or `FAILURE`) and shipments whose `displayStatus` is `CANCELED` are not taken into account.
- The table also appears in the **How statuses are matched** section of the connection card in the panel.
- If a value that is not in the table arrives, the order or line is not read and is counted on the **Could not be read** line.

## Delivery rule

- A line counts as **Delivered** when the shipment it is in has the status `DELIVERED` or `PICKED_UP`. The delivery date is the shipment's `deliveredAt`, or its `updatedAt` when that is empty.
- If a line is split over several shipments, the shipment with the furthest status is used.
- A shipment's status reaches Shopify from carrier tracking or from apps that add delivery events to the shipment. Shipments that never get a delivery in Shopify stay **Shipped** or **In transit**, and no invitation is sent for those lines.
- For invitations and the right to write a review, the line must be **Delivered** and the order **Paid** or **Partially refunded**. The invitation is sent once the waiting time on the **Invitation sending** screen has passed.

## Orders that could not be read

| In the panel | Reason |
|---|---|
| number or date missing | The order has no `name`, no `createdAt` or no list of product lines |
| status not recognised | If the order's payment status is not in the table the order is skipped; if a line's shipment status is not, only that line is skipped |
| product code missing on a line | The line has no identifier, or both `sku` and the variant are empty; the line is skipped |
| no readable product lines | None of the order's lines could be read |
| more than 50 product lines; the first 50 were read | The order's first 50 lines were saved and the remaining lines were not read |
| could not be saved in Sayproof | The order was read but could not be saved in Sayproof (for example, there is no rule for the channel's market) |

Orders that could not be read or saved do not stop the read; the other orders are saved. If none of the orders read can be saved, the read counts as failed and its position does not move; the reason appears on the **Last error** line.

## Failures

| In the panel | Meaning |
|---|---|
| the address is not a Shopify store address; write it as yourbrand.myshopify.com | The address is not of the form `<store>.myshopify.com`; no request is sent |
| the keys needed for the connection are missing | The **Client secret** is empty |
| the platform did not accept the keys | Shopify refused the token request (`400`, `401`, `403`) or did not accept the token (`401`) |
| the app or key has no permission to read orders | The app has no `read_orders` scope or the scope was not approved on the store |
| requests were sent too often | Shopify reported that the request limit had been reached (on the first page) |
| the domain could not be reached | The store address could not be resolved |
| couldn't connect to the server, a secure connection could not be made | The connection or the TLS handshake failed |
| the source did not answer in time | The request got no answer within 30 seconds |
| the address redirects elsewhere; enter the direct address | Shopify returned a redirect; redirects are not followed |
| the other system returned an error, status code | Shopify answered with something other than `200` (for example `http_status: 402` if the store is frozen, `http_status: 423` if it is unavailable) |
| the platform's answer could not be read, the platform's answer is too large | The answer is not JSON, does not contain the expected order list or is larger than 20 MB |
| the stored sign-in details couldn't be read; enter them again | The saved details could not be decrypted; enter them again with **Change the sign-in details** |
| the read stopped with an unexpected error | An unexpected error occurred; the read is tried again after the read interval |

## Showing reviews on the product page

Reviews are shown on the product page with the [script](/en/integrate/script.md). In Shopify, the script and the container are added to the theme's product template with a **Custom Liquid** block:

1. In the Shopify admin, under **Online Store › Themes**, press **Customize** on your theme.
2. Open the product template (**Products › Default product**) from the page picker at the top.
3. In the section where the reviews should appear, add a **Custom Liquid** block with **Add block**, paste the code below and press **Save**.

```html
<script type="module"
        src="https://cdn.sayproof.example/sayproof.js"
        data-review-site-key="YOUR-SITE-KEY"
        data-review-base="https://api.sayproof.example"
        data-review-locale="en"></script>
<div data-review-mount
     data-review-product="{{ product.selected_or_first_available_variant.sku | default: product.selected_or_first_available_variant.id }}"
     data-review-product-name="{{ product.title | escape }}"></div>
```

- The `src` and `data-review-site-key` values are in the **Values for the setup** list on the **Show reviews on the product page** card on the **Integration › Showing reviews** tab in the panel when the **Script (in the browser)** route is chosen.
- `data-review-product` is the SKU of the variant selected when the page opens (or the first available one); when the SKU is empty, the variant number is used. This matches the product code Sayproof reads from the order.
- The theme does not update the container when a variant is chosen. To show the reviews of all of the product's variants together, add `data-review-scope="group"` and set `data-review-product` to `{{ product.id }}`; the group code is the product number in Shopify.

## Domain verification

Reviews only appear on pages of verified domains. Because files cannot be placed at the site root in Shopify, the domain is verified with a DNS record:

1. Add the domain in the panel; for example `www.yourbrand.com`. See [Domain verification](/en/integrate/channels-and-keys.md#domain-verification).
2. In the "How to verify `<domain>`" section, choose **DNS record** as the **Verification method**. In your domain's DNS settings, add a `TXT` record with the **Name** (`_sayproof-verification.www.yourbrand.com`) and **Value** the panel shows. If the domain was bought through Shopify, its DNS records are edited in the Shopify admin under **Settings › Domains**.
3. Once the record is added, press **Verify** in the panel. The record can take from a few minutes to a few hours to appear.

Each domain where reviews are shown is verified separately. If customers see your own domain rather than the `myshopify.com` address, verify that domain.

## Security

- The Client secret (or the earlier custom app's Admin API access token) is stored encrypted and never shown again in the panel or in API answers. The token Shopify gives during a read is not stored.
- Only `https://<store>.myshopify.com` is contacted; private network addresses and redirects are refused.
- The app only has read access; Sayproof changes nothing in Shopify. To cut the connection completely, uninstall the app from your store.
- Connections are only seen and managed by a user signed in to the panel with the "Can change settings" permission; these actions cannot be taken with an API key.
- Setting up, changing, trying, reading manually and removing are recorded on the **Activity history** screen.
