# Orders

Read Shopify and WooCommerce orders in one shape, with line items, tax, refunds and attribution, and keep them in sync with webhooks instead of polling.

Orders from Shopify and WooCommerce come back in the same shape: line items with unit prices, tax per line, discounts, shipping and fee lines, billing details, payment status, refunds and where the buyer came from. That is enough to rebuild an invoice, feed an analytics tool or attribute a sale to a post without knowing which store it came from. Orders are read-only.

## Before you start

| Store | What it needs |
| --- | --- |
| Shopify | The `orders.read` capability, which needs the optional scopes `read_orders` and `read_all_orders`. Without `read_all_orders`, Shopify returns only the last 60 days. Check with `GET /v1/commerce/store` and send the owner to `grantPermissionsUrl` |
| WooCommerce | Nothing extra: any user who can manage WooCommerce can read orders |

Shopify also gates buyer details behind its protected customer data approval. Where the approval does not cover them, `customer`, `billingAddress` and `shippingAddress` are `null` and the rest of the order is complete.

## List orders

Lists come newest first. Filter by `createdAfter` and `createdBefore`, or pass `updatedAfter` to page forward from the last change you saw: results then come oldest first. Shopify pages hold up to 25 orders, WooCommerce pages up to 100.

```bash cURL
curl "https://api.pozy.ai/v1/commerce/orders?accountId=6710c4b2e4b0d2f1a8c95e3a&updatedAfter=2026-10-29T00:00:00Z&limit=25" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/commerce/orders?accountId=6710c4b2e4b0d2f1a8c95e3a&updatedAfter=2026-10-29T00:00:00Z&limit=25", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const o of data.orders) console.log(o.number, o.status, o.total.amount, o.total.currency);
```
```python Python
import os, requests

orders = requests.get(
    "https://api.pozy.ai/v1/commerce/orders",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"accountId": "6710c4b2e4b0d2f1a8c95e3a", "updatedAfter": "2026-10-29T00:00:00Z", "limit": 25},
).json()["orders"]
```

```json
{
  "id": "6148123456",
  "accountId": "6710c4b2e4b0d2f1a8c95e3a",
  "platform": "shopify",
  "number": "#1042",
  "status": "awaiting_fulfillment",
  "platformStatus": "unfulfilled",
  "paymentStatus": "paid",
  "currency": "EUR",
  "taxesIncluded": true,
  "subtotal": { "amount": "58.00", "currency": "EUR" },
  "discountTotal": { "amount": "8.70", "currency": "EUR" },
  "shippingTotal": { "amount": "4.90", "currency": "EUR" },
  "taxTotal": { "amount": "9.40", "currency": "EUR" },
  "total": { "amount": "54.20", "currency": "EUR" },
  "discountCodes": ["INSTA15"],
  "lineItems": [
    {
      "id": "15011223344",
      "productId": "8214569812345",
      "variantId": "45123456789012",
      "sku": "BEANIE-GRY",
      "title": "Merino beanie",
      "quantity": 2,
      "unitPrice": { "amount": "29.00", "currency": "EUR" },
      "total": { "amount": "49.30", "currency": "EUR" }
    }
  ],
  "refunds": [],
  "createdAt": "2026-10-29T14:02:11Z",
  "updatedAt": "2026-10-29T14:02:15Z"
}
```

Get one order with `GET /v1/commerce/orders/{orderId}?accountId=...`.

## Read the fields

| Field | Meaning |
| --- | --- |
| `status` | `pending_payment`, `on_hold`, `awaiting_fulfillment`, `partially_fulfilled`, `in_transit`, `delivered`, `completed` or `cancelled`. Raw value in `platformStatus` |
| `paymentStatus` | `pending`, `authorized`, `partially_paid`, `paid`, `partially_refunded`, `refunded`, `voided` or `failed` |
| `subtotal`, `unitPrice` | Before discounts |
| `total` | After discounts. `taxesIncluded` says whether amounts include tax |
| `lineItems[].taxLines`, `shippingLines`, `feeLines`, `taxLines` | Tax and charges per line, each with `title`, `ratePercent` and `amount` |
| `refunds` | Each refund with `amount`, `note` and the refunded line items. `refundedTotal` sums them |
| `fulfillments` | Carrier, tracking number and tracking URL |
| `customer`, `billingAddress`, `shippingAddress` | Buyer details, or `null` where the store does not share them |
| `buyerNote`, `discountCodes`, `paymentMethods` | As entered at checkout |

All amounts are in the store currency as decimal strings.

## Attribution

`attribution` says where the order came from. Use it to credit a sale to a post, a campaign or a channel.

```json
"attribution": {
  "ready": true,
  "firstVisit": null,
  "lastVisit": {
    "occurredAt": "2026-10-29T13:55:00Z",
    "landingUrl": "https://acme.com/products/merino-beanie?utm_source=instagram&utm_campaign=winter",
    "referrerUrl": "https://l.instagram.com/",
    "source": "instagram",
    "sourceType": "SOCIAL",
    "utm": { "source": "instagram", "medium": "social", "campaign": "winter", "term": null, "content": null }
  },
  "customAttributes": [{ "key": "gift_message", "value": "Happy birthday" }]
}
```

| Store | What it records |
| --- | --- |
| Shopify | First and last visit from the customer journey. Shopify attributes a new order shortly after it is placed: until then `ready` is `false` and both visits are `null`. No event fires when it becomes ready, so read the order again later |
| WooCommerce | The last visit only (`firstVisit` and `occurredAt` are always `null`). Needs WooCommerce 8.5 or later with Order Attribution turned on, for orders placed after that |

`customAttributes` holds cart and note attributes on Shopify and public order meta on WooCommerce.

> **Tag your links:**
> 
> Attribution is only as good as your UTM parameters. Put `utm_source`, `utm_medium` and `utm_campaign` on every link you publish, and you can group orders by post or campaign straight from `lastVisit.utm`.

## Keep in sync with webhooks

Polling works, but webhooks are cheaper. When a store grants order access, Pozy subscribes to its order changes (on WooCommerce, when the site is connected). Subscribe your [webhook endpoint](https://pozy.ai/docs/webhooks) to the three events below. They carry the store and the order id; fetch the full order by id.

```json
{
  "id": "evt_6f1c2a9b8d7e4c3a",
  "event": "commerce.order.created",
  "timestamp": "2026-10-29T14:02:16.000Z",
  "store": { "accountId": "6710c4b2e4b0d2f1a8c95e3a", "platform": "shopify", "shopId": "64822345678" },
  "resource": { "type": "order", "id": "6148123456", "status": "awaiting_fulfillment", "platformStatus": "unfulfilled" }
}
```

A refund usually fires `commerce.order.updated` as well as `commerce.order.refunded`. A WooCommerce site connected before order webhooks were available gets them the next time it reconnects or a catalog sync is created.

| Event | When it fires |
| --- | --- |
| [`commerce.order.created`](https://pozy.ai/docs/webhooks/events#commerce-order-created) | Commerce order created. |
| [`commerce.order.updated`](https://pozy.ai/docs/webhooks/events#commerce-order-updated) | Commerce order updated. |
| [`commerce.order.refunded`](https://pozy.ai/docs/webhooks/events#commerce-order-refunded) | Commerce order refunded. |

## Sync without gaps

To backfill or recover after downtime, page forward with `updatedAfter`:

1. Store the newest `updatedAt` you have processed.
2. Start each sync a few minutes before it, because an order edited while you page can shift the list.
3. Drop duplicates by `id`.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/commerce/orders`](https://pozy.ai/docs/api/listCommerceOrders) | List orders |
| [`GET /v1/commerce/orders/{orderId}`](https://pozy.ai/docs/api/getCommerceOrder) | Get an order |
| [`GET /v1/commerce/store`](https://pozy.ai/docs/api/getCommerceStore) | Get a store |
