# Shopify

Source module: `app/connectors/shopify.py`. Kind: **facts** only. Display name: "Shopify".

The order system in the flagship refund story: the agent refunds in Stripe and the order must show as cancelled
here. The connector reads orders changed since the watermark through the Admin GraphQL API and turns each into a
state fact plus event facts, keyed by the order's numeric id, which is what an agent's tools see.

## Auth and scopes

| Item | Value |
|---|---|
| Method | Admin API access token of a custom app, sent as `X-Shopify-Access-Token`, against `https://{shop}/admin/api/{version}/graphql.json` (version `2026-07`; override with `api_version` in the credentials) |
| Credentials stored (encrypted) | `shop` (the `*.myshopify.com` domain), `access_token`; optional `api_version` |
| Least-privilege scopes | **read_orders**. Add **read_customers** only if you want customer identities hashed for cross-system linking; without it Shopify refuses the `customer` field with `ACCESS_DENIED`, the connector sends the orders query again without it, `customer_hid` is null, and the connection's health says so. Nothing else. |
| Connection test | `{ shop { name myshopifyDomain } }` |
| How to get the token | Settings → Apps and sales channels → Develop apps → Create an app → Configure Admin API scopes (read_orders) → Install app → Reveal the Admin API access token once |

## Fields pulled and why

One query, `orders(first: 100, after, query: "updated_at:>='<watermark>'", sortKey: UPDATED_AT)`, paged by cursor:

| Field | Used for |
|---|---|
| `legacyResourceId`, `id`, `name` | Subject id (numeric order id), `order_name` attribute |
| `createdAt`, `updatedAt` | State fact timestamp, watermark |
| `cancelledAt`, `cancelReason` | `order_cancelled` fact |
| `displayFinancialStatus`, `displayFulfillmentStatus` | State attributes and the derived `status` |
| `customer.email` | HMAC-hashed into `customer_hid`; the email is discarded |
| `refunds { id createdAt totalRefundedSet { shopMoney { amount currencyCode } } }` | `refund_created` facts |
| `fulfillments { id createdAt status }` | `fulfillment_created` facts |

Line items, addresses, names, phone numbers, notes, payment details and metafields are never requested.

## Fact types produced

Subject id is the numeric order id (`10231`), subject kind `order`.

| `fact_type` | When | Attributes | `raw_ref` |
|---|---|---|---|
| `order_status` | Every order returned (one per `updatedAt`) | `status` (`open`, `cancelled`, `refunded`, `partially_refunded`), `financial_status`, `fulfillment_status`, `order_name`, `customer_hid` | `shopify:order:{id}:status:{updatedAt}` |
| `order_cancelled` | `cancelledAt` set | `reason`, `customer_hid` | `shopify:order:{id}:cancelled` |
| `refund_created` | Each refund on the order | `amount` (major units), `currency`, `customer_hid` | `shopify:refund:{refund_id}` |
| `fulfillment_created` | Each fulfilment | `status` | `shopify:fulfillment:{fulfillment_id}` |

`status` is derived from fields Shopify reports and nothing else: cancelled when `cancelledAt` is set, else the
refund state from the financial status, else `open`. The library `refund_workflow` template's precondition
`state_before_action: {source: shopify, type: order_status, where: {status: eligible}}` is a placeholder for your
own policy: with this connector write it against the real fields, for example
`where: {financial_status: paid}` or `where: {status: open}`.

## Incremental sync, watermark, throttling

- First sync reads every order; later syncs add `updated_at:>='<watermark>'`. The watermark is the newest
  `updatedAt` seen, then advanced to the sync time less the safety margin like every connector.
- Shopify throttles GraphQL by query cost with HTTP 200 and a `THROTTLED` error; the connector backs off and
  retries up to six times. HTTP 401/403 marks the connection for re-authentication.
- Upserts are idempotent on `raw_ref`; a re-sync changes nothing that did not change.

## Claims produced

None.

## Limitations

- Orders only. Draft orders, returns (as distinct from refunds), inventory and customer records are not read.
- `refund_created` carries the refund's total; per-line refund detail is not read.
- Orders archived or deleted in Shopify stop appearing in `updated_at` queries; facts already written remain.
