# Shopify

Connect a Shopify store to manage its catalog, read its orders, publish blog articles and install ad pixels on the storefront.

Connecting Shopify gives you a store account, which is a different kind of account from a social one. Through it you run the catalog (products, stock, collections, discounts), read orders, publish blog articles and install ad pixels on the storefront. You cannot pick a store as a destination in `POST /v1/posts`, and it has no inbox or social analytics.

## What you can do

| Job | How | Page |
| --- | --- | --- |
| Manage products, variants, images and stock | Commerce API (`/v1/commerce/...`) | [Products](https://pozy.ai/docs/commerce/products) |
| Read orders with line items, tax, refunds and attribution | Commerce API, read-only | [Orders](https://pozy.ai/docs/commerce/orders) |
| Collections, discounts, pages, menus, redirects, markets, metafields | Commerce API | [Commerce overview](https://pozy.ai/docs/commerce) |
| Keep a Meta product catalog in sync for catalog ads | Catalog syncs | [Products](https://pozy.ai/docs/commerce/products#sync-a-store-into-a-meta-catalog) |
| Create, schedule and edit blog articles | Blogs API | [Blogs](https://pozy.ai/docs/commerce/blogs) |
| Fire Meta, TikTok, Google, LinkedIn, Pinterest, X and ChatGPT pixels | Tracking tag install | [Site pixels](https://pozy.ai/docs/commerce/site-pixels) |

| Property | Value |
| --- | --- |
| Platform id | `shopify` |
| Connect | OAuth install (needs the store's `myshopify.com` domain) or a custom-app Admin token |
| Social posting | No |
| Analytics | No (order data comes from the Commerce API) |
| Ids | Shopify's own numeric ids as strings. Read them from responses, never build them |

## Connect a store

Shopify cannot look up a merchant's stores for you, so you need the store domain first. Ask the merchant for it (`acme.myshopify.com`, or just `acme`).

```bash cURL
curl "https://api.pozy.ai/v1/connect/shopify?profileId=6710c3a9e4b0d2f1a8c95e21&shop=acme.myshopify.com&redirect_url=https://app.example.com/connected" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/shopify?profileId=6710c3a9e4b0d2f1a8c95e21&shop=acme.myshopify.com&redirect_url=https:%2F%2Fapp.example.com%2Fconnected", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
// Redirect the merchant to data.authUrl
```
```python Python
import os, requests

resp = requests.get(
    "https://api.pozy.ai/v1/connect/shopify",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={
        "profileId": "6710c3a9e4b0d2f1a8c95e21",
        "shop": "acme.myshopify.com",
        "redirect_url": "https://app.example.com/connected",
    },
)
auth_url = resp.json()["authUrl"]
```

Open `authUrl` in the merchant's browser. When they approve the install, Shopify and Pozy send them to your `redirect_url`, whose query string holds `connected`, `profileId`, `accountId` and `username`, the store's domain. Keep the `accountId`. Rely on the redirect rather than on the domain you sent: the merchant can end up approving another store.

`redirect_url` has to be a full `http(s)` URL or a custom app scheme like `myapp://connected`; a relative path gets `400`. If you connect a store again to the profile it is already in, Pozy just renews the existing account's token.

### Connect with an Admin token instead

Without a browser step, the merchant sets up a custom app in the Shopify admin under **Settings > Apps and sales channels > Develop apps**. They give it `read_content`, `write_content`, `read_products` and `write_products`, then hand you its Admin API token, which begins with `shpat_`.

```bash cURL
curl -X POST https://api.pozy.ai/v1/connect/shopify/token \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6710c3a9e4b0d2f1a8c95e21",
    "shop": "acme.myshopify.com",
    "accessToken": "shpat_..."
  }'
```

Pozy checks the token against the store before it saves anything. Custom-app tokens do not expire. Two things work differently with a token: the store cannot grant optional permissions through Pozy (the merchant adds scopes to the custom app instead), and pixels cannot be installed, because a Shopify web pixel belongs to the app that installs it.

## Permissions

The install asks for the scopes Pozy needs for content and catalog work. Nothing about customers or payments is requested up front.

| Scope | Enables |
| --- | --- |
| `read_content`, `write_content` | Blogs, articles and online store pages |
| `read_products`, `write_products` | Products, variants, images, collections and metafields |
| `write_pixels`, `read_customer_events` | Installing pixels and letting them receive storefront events |

Stock, sales channels, discounts, navigation, metaobjects, markets, marketing activities, image removal and orders need optional scopes. The merchant approves those later without reinstalling. Call `GET /v1/commerce/store`: it lists what the store can do now in `capabilities`, what is missing in `missingCapabilities`, and returns a `grantPermissionsUrl` to send the store owner to.

| Capability | Shopify scope behind it |
| --- | --- |
| `products.images_remove` | `write_files` |
| `inventory.read`, `inventory.write` | `write_inventory` and `read_locations` |
| `channels.read`, `channels.write` | `write_publications` |
| `discounts.read`, `discounts.write`, `discounts.codes` | `write_discounts` |
| `navigation.read`, `navigation.write` | `write_online_store_navigation` |
| `metaobjects.read`, `metaobjects.write` | `write_metaobjects` |
| `markets.read`, `markets.write` | `write_markets` (writes also use `write_products`) |
| `marketing.write` | `write_marketing_events` |
| `orders.read` | `read_orders` and `read_all_orders` |

> **Orders need two scopes:**
> 
> Without `read_all_orders`, Shopify only returns the last 60 days of orders, so a store that granted only `read_orders` still lists `orders.read` as missing. Order reads also need Shopify's approval for protected customer data. Where that approval does not cover buyer details, `customer`, `billingAddress` and `shippingAddress` come back as `null`.

## Shopify-only operations

Most of the Commerce API works on both Shopify and WooCommerce. These operations exist only on Shopify and return `400 platform_not_supported` elsewhere:

- Sales channels: list channels, publish or unpublish products and collections
- Markets and price lists, including fixed prices per market
- Metaobjects and their definitions
- Navigation menus and URL redirects
- Collection metafields and reordering products inside a collection
- Adding codes to an existing discount
- Marketing activities: log a post, ad or message you ran for the merchant so it shows in the store's Marketing area with its UTM tags, then add engagement numbers to it each day

```bash cURL
curl -X PUT https://api.pozy.ai/v1/commerce/marketing-activities \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6710c4b2e4b0d2f1a8c95e3a",
    "remoteId": "post_6711a0c3e4b0d2f1a8c96001",
    "title": "Autumn drop on Instagram",
    "url": "https://acme.com/collections/autumn",
    "utm": { "campaign": "autumn-drop", "source": "instagram", "medium": "social" },
    "tactic": "post",
    "channel": "social",
    "status": "active"
  }'
```

Set `remoteId` to an id from your side, such as the Pozy post id. A second call with that `remoteId` changes the existing activity instead of adding one.

## Limits and behaviour

- Pozy keeps no copy of store data. Every read goes to the store, except the catalog syncs you create.
- Shopify tells Pozy about product changes through its webhooks, so synced catalogs catch up within minutes and you get `commerce.product.*` events.
- When the merchant uninstalls the app, Shopify revokes the token at once and Pozy marks the account disconnected. There is nothing for you to revoke. Reinstalling runs the normal connect flow and issues a fresh token.
- Blog and article deletes are permanent. Deleting a blog deletes every article in it.
- You cannot create orders, write customers, upload media or publish social posts through a store account.

## Errors

| Error | Cause | Fix |
| --- | --- | --- |
| `400` on connect | `shop` is not a `myshopify.com` domain, or `redirect_url` is relative | Pass `acme.myshopify.com` (or `acme`) and an absolute redirect |
| `403 insufficient_permissions` | The store never granted the needed scope, or Shopify turned the call down | Point the owner to `grantPermissionsUrl`, or connect the store again |
| `409 reconnect_required` | A pixel install on a store connected with a token or without pixel access | Send the merchant to `details.authUrl`. The account keeps its id |
| `404 product_not_found` | The product was deleted or belongs to another store | List products for a current id |
| `404 blog_article_not_found` | The article was deleted or is in another blog | List the blog's articles |
| `429` | Your Pozy rate limit or Shopify's own limit | Wait for `Retry-After`, then retry |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/shopify`](https://pozy.ai/docs/api/getShopifyConnectUrl) | Get Shopify OAuth connect URL |
| [`POST /v1/connect/shopify/token`](https://pozy.ai/docs/api/connectShopifyWithToken) | Connect a Shopify store with a custom-app Admin token |
| [`GET /v1/commerce/store`](https://pozy.ai/docs/api/getCommerceStore) | Get a store |

The full catalog, order and discount surface is listed on [Commerce](https://pozy.ai/docs/commerce#endpoints), and the article endpoints on [Blogs](https://pozy.ai/docs/commerce/blogs#endpoints).

## Webhook events

| Event | When it fires |
| --- | --- |
| [`account.connected`](https://pozy.ai/docs/webhooks/events#account-connected) | Account connected. |
| [`account.disconnected`](https://pozy.ai/docs/webhooks/events#account-disconnected) | Account disconnected. |
| [`commerce.product.created`](https://pozy.ai/docs/webhooks/events#commerce-product-created) | Commerce product created. |
| [`commerce.product.updated`](https://pozy.ai/docs/webhooks/events#commerce-product-updated) | Commerce product updated. |
| [`commerce.product.deleted`](https://pozy.ai/docs/webhooks/events#commerce-product-deleted) | Commerce product deleted. |
| [`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. |
