# Stocky API v2 — reference
Captured 8 August 2026 from `https://stocky.shopifyapps.com/api/docs/v2.html`.
Kept locally because the app **and its documentation** go offline on
**31 August 2026**.
---
## Authentication
Two headers. No OAuth, no token exchange.
```
GET https://stocky.shopifyapps.com/api/v2/purchase_orders.json
Store-Name: example.myshopify.com
Authorization: API KEY=abcdefghijklmnopqrstuvwxyz123456
```
The merchant generates the key at `https://stocky.shopifyapps.com/preferences/api`
while signed in.
**The session cookie does not work.** Verified 8 Aug 2026: a request to
`/api/v2/tax_types.json` from a signed-in browser tab, with the session cookie
sent and no `Authorization` header, returns **401**. The API key is mandatory
and there is no alternative — no other API version exists, and nothing returned
403 during a full probe, so there are no gated routes to unlock instead.
**The API may be switched off on the store.** Third-party integration docs
(Report Pundit, Jan 2026) instruct merchants to request the key by contacting
Shopify: only once Support enables the option does the key appear under
Preferences → API access. So a merchant may open that page and find nothing.
Put those two facts together and the consequence is sharp: **if API access is
not enabled, part of the data is unreachable by any means.** Session-based
extraction does not substitute for the key, because purchase orders with their
line items, supplier `id`, `company_name`, `phone_toll_free` and the timestamps
exist only in the API. The two paths are complementary, not alternatives.
Operationally, this is the one step with an external dependency and an
unpredictable turnaround. Ask a merchant to open `/preferences/api` and say what
they see **before** anything else. If the section is missing, open a Shopify
Support ticket that same day — everything else in this process takes about an
hour.
**CORS is open.** Verified 8 Aug 2026 from an unrelated origin: the preflight
passes and `Authorization` / `Store-Name` are both allowed, so a browser page on
any domain can call this API directly. This is unusual for an internal API and
should not be assumed to survive.
---
## The API is read-only
Every documented endpoint is a `GET`. There are no `POST`, `PATCH` or `DELETE`
operations — confirmed both by the resource list and by Shopify Partners on the
community forum. Nothing built against this API can modify merchant data.
---
## Resources
| Resource | Endpoints |
| --- | --- |
| Purchase Order | `GET /api/v2/purchase_orders.json`
`GET /api/v2/purchase_orders/{id}.json` |
| Suppliers | `GET /api/v2/suppliers.json`
`GET /api/v2/suppliers/{id}.json` |
| Stock Adjustment | `GET /api/v2/stock_adjustments.json`
`GET /api/v2/stock_adjustments/{id}.json` |
| Stock Adjustment Item | `GET /api/v2/stock_adjustment_items.json`
`GET /api/v2/stock_adjustment_items/{id}.json` |
| Tax Type | `GET /api/v2/tax_types.json`
`GET /api/v2/tax_types/{id}.json` |
| **Stock Transfer** *(undocumented)* | `GET /api/v2/stock_transfers.json` |
### Probe results, 8 Aug 2026
`/api/v2/stock_transfers.json` is absent from the documentation index but
responds 200 with a `{"stock_transfers": [...]}` envelope. Note the asymmetry:
the HTML route `/stock_transfers` now redirects into the Shopify admin because
the feature has migrated, yet the API to that data still answers. On a store
with transfer history this may be the only remaining way to read it — worth
checking early, since the interface is already gone.
Everything else probed came back 404. Specifically **there is no vendors
endpoint** — not `vendors`, `vendor`, `vendor_products`, `lead_times`, `limits`
or `min_orders`. Also absent: `products`, `variants`, `locations`, `stock_takes`,
`settings`, `preferences`, `custom_fields`, `users`, `shop`, `account`,
`purchase_items`.
No other API version exists: `/api/v1`, `/api/v3` and unversioned `/api` all 404.
And nothing returned 401 or 403 during the probe, so there are no gated routes
worth trying to unlock — a route either exists or it does not.
**Consequence for vendors.** Lead times live on the vendor, and the API cannot
enumerate vendors at all, so their ids have to come from the HTML. The best
source is the vendor filter select on `/reports/variants`
(`select[name="q[product_vendor_id_eq]"]`): it renders `` for the full set, giving id and name together in one request,
which neither the list page nor `vendors.csv` does.
---
## Pagination — read this before writing a loop
`since_id` **does not mean the same thing on every resource.**
| Resource | Documented meaning of `since_id` | Cursor advances on |
| --- | --- | --- |
| `suppliers` | IDs **greater than** the provided ID | `max(ids)` |
| `purchase_orders` | IDs **less than** the provided ID | `min(ids)` |
| others | not documented | detect from ordering |
Purchase orders come back **newest first** (the official example response lists
`id: 2` before `id: 1`). A loop written for the suppliers semantics will fetch
the first page of purchase orders and then stop, appearing to succeed. On a
store with three years of history that silently loses everything but the most
recent 250 orders.
Defensive pattern that works regardless:
1. Track every id already seen.
2. Infer direction from the first page: `ids[0] > ids[-1]` means descending, so
use `min`; otherwise `max`.
3. Stop when a page contains no id you have not already seen.
With the dedupe guard in place, guessing the direction wrong degrades to a slow
one-record-per-page crawl instead of truncating or looping forever.
`offset` also exists but the docs explicitly say not to use it for pagination,
and it is ignored when `since_id` is present.
**`show` returns nothing extra.** Checked against the documentation on
8 Aug 2026: `GET /api/v2/suppliers/{id}.json` returns exactly the same 18 fields
as the list endpoint, unwrapped (a bare object instead of a `suppliers` array).
No lead time, no MOQ, no payment terms. The natural assumption that a Rails
`show` action carries a richer serializer than `index` does not hold here — so
there is no point walking every supplier id hoping for more.
**Avoid `GET /api/v2/purchase_orders/{id}.json`.** A merchant reported on the
Shopify community that the endpoint returns 404 for purchase orders that the
list endpoint had just returned. It is also unnecessary: the list response
already embeds `purchase_items` in full.
---
## Purchase Order
### `GET /api/v2/purchase_orders.json`
**Parameters**
| Param | Notes |
| --- | --- |
| `confirmed_since` | ISO 8601. Orders confirmed on or after this date. |
| `updated_since` | ISO 8601. Orders updated on or after this date. |
| `status` | `draft`, `draft-archived`, `draft-unarchived`, `confirmed`, `confirmed-archived`, `confirmed-unarchived`, `archived`, `unarchived` |
| `limit` | Records per page. |
| `offset` | Present, but not for pagination. |
| `since_id` | IDs **less than** the provided ID. |
**Fields**
| Field | Type | Notes |
| --- | --- | --- |
| `id` | Integer | |
| `number` | String | Merchant-overridable. Falls back to `sequential_id`. |
| `sequential_id` | Integer | |
| `invoice_number` | String | Supplier's own reference. |
| `created_at` / `updated_at` / `generated_at` | DateTime | ISO 8601 |
| `ordered_at` | DateTime | **Null means the PO was never confirmed.** |
| `expected_on` / `ship_on` / `payment_due_on` / `invoice_date` | Date | |
| `archived` | Boolean | |
| `paid` | Boolean | |
| `supplier_name` | String | |
| `supplier_id` | String | Optional. Absent when the PO is tied to a *vendor* rather than a supplier record — in that case only `supplier_name` identifies it. |
| `currency` | String | Supplier's currency. |
| `adjustments` / `adjustments_local` | Decimal | Supplier's currency / merchant's currency. |
| `shipping` / `shipping_local` | Decimal | Same split. |
| `shipping_tax_type` | Integer | Look up the rate via the tax type endpoint. |
| `shopify_receive_location_id` | Integer | Maps to a Shopify Location. |
| `purchase_items` | Array | Embedded line items, see below. |
**`purchase_items[]`**
| Field | Type | Notes |
| --- | --- | --- |
| `id` | Integer | |
| `sku` | String | Can be empty. |
| `inventory_item_id` | Integer | Shopify InventoryItem — the join key back into Shopify. |
| `product_title` / `variant_title` | String | |
| `asin` | String | Amazon identifier. |
| `quantity` | Integer | |
| `status` | String | e.g. `not delivered` |
| `retail_price` | String | |
| `cost_price` | String | What was paid. Frequently `null`. |
| `supplier_cost_price` | String | Frequently `null`. |
| `account_code` | String | Accounting code, e.g. `300`. |
| `tax_type_id` | Integer | |
| `accounting_tax_type` | String | e.g. `HST` |
| `received_at` | Date | **Null means not yet received.** |
| `updated_at` | String | ⚠ Not ISO. Rendered as `DD/MM/YYYY HH:MM` using the merchant's own date format from Preferences → General. Parse accordingly, or you will silently swap days and months. |
---
## Suppliers
### `GET /api/v2/suppliers.json`
**Parameters:** `limit` (default 50, max 250), `since_id` (IDs **greater than**),
`updated_since` (ISO 8601).
**Fields**
| Field | Type |
| --- | --- |
| `id` | Integer |
| `name` | String |
| `company_name` | String |
| `account_number` | String |
| `contact_name` | String |
| `contact_email` | String |
| `address1` / `address2` | String |
| `city` / `province_code` / `country_name` / `zip` | String |
| `phone` / `phone_toll_free` / `fax` | String |
| `is_hidden` | Boolean |
| `created_at` / `updated_at` | String, ISO 8601 |
**This contradicts Shopify's own migration guidance.** The help centre says
suppliers "can't be exported from Stocky", and every third-party migration guide
repeats it, telling merchants to retype their supplier list by hand. That is true
of the *interface* — there is no export button. The API returns the full record.
**What is genuinely absent:** no `lead_time`, no minimum order quantity, no
payment terms, no per-product supplier assignment. Those exist in the Stocky UI
but appear nowhere in the API. They have to be captured from the interface.
---
## Stock Adjustment / Stock Adjustment Item
`GET /api/v2/stock_adjustments.json` and
`GET /api/v2/stock_adjustment_items.json`, each with a `{id}` variant.
Field lists were not captured before the deadline; probe with `limit=1` against a
live store to see the real shape. Adjustment *reasons* are configured in Stocky
(Inventory → Adjustments) and, per merchant reports, do not sync to Shopify —
they surface there only as "Stocky". If reasons matter for tax records, verify
whether the API returns them and capture them from Reports → Adjustments if not.
---
## Tax Type
`GET /api/v2/tax_types.json`, plus `{id}` variant. Needed to resolve
`shipping_tax_type` and `purchase_items[].tax_type_id` into actual rates. Small
table — fetch it and join locally.
---
## The UI has CSV exports the API does not — and vice versa
Verified 8 Aug 2026 on a live store. Both exist:
- **Vendors** page → `CSV` button (top right)
- **Suppliers** page → `More actions ▾` → export
**Shopify's migration guide is wrong on this point.** It states plainly that
"Suppliers can't be exported from Stocky", and every third-party migration guide
repeats it, telling merchants to retype their supplier list by hand. The export
button is there.
Both files share the same 17 columns:
```
Name, Address 1, Address 2, City, Province, Zip, Country, Phone,
Contact name, Contact email, Website, Payment terms, Payment method,
Currency, Account number, Fax number, Notes
```
Observed values: `Payment terms` is an enum-like slug (`net_30`, `pre_paid`),
`Payment method` likewise (`credit_card`), `Currency` can be `base` meaning the
merchant's own currency.
### Coverage matrix — supplier record
| Field | API | UI CSV |
| --- | :-: | :-: |
| `id` | yes | **no** |
| Name | yes | yes |
| `company_name` | yes | **no** |
| Address 1 / 2, City, Province, Zip, Country | yes | yes |
| Phone | yes | yes |
| `phone_toll_free` | yes | **no** |
| Fax | yes | yes |
| Contact name / email | yes | yes |
| Account number | yes | yes |
| **Website** | **no** | yes |
| **Payment terms** | **no** | yes |
| **Payment method** | **no** | yes |
| **Currency** | **no** | yes |
| **Notes** | **no** | yes |
| `is_hidden` | yes | **no** |
| `created_at` / `updated_at` | yes | **no** |
Neither source is complete. A full supplier record requires both, joined — and
**the only shared key is the name**, because the CSV carries no id. Count
duplicate names before joining and surface collisions to the merchant rather
than merging silently; two suppliers called the same thing are not rare in
retail.
### Vendors are a separate entity, and lead times live on them
Stocky models vendors and suppliers separately. The API exposes suppliers only —
there is no vendors endpoint at all. This is why `purchase_orders.supplier_id` is
optional: a PO tied to a vendor rather than a supplier record is identified by
the `supplier_name` string alone.
The vendors CSV has the identical 17-column schema, so it does **not** include
lead times. Per the app's own UI, lead times are set per Shopify Vendor or per
SKU, under Vendors → All vendors → select a vendor → Settings → Lead & Re-stock
Times. That screen is covered by no export. The UI also promises lead times will
move "within the supplier entity soon" — that will not happen; the app is
switched off on 31 August 2026.
Mapping consequence: Shopify recommends `supplier.lead_time_days` as a **variant**
metafield, while Stocky holds the value on the vendor. Vendor-level values must
be fanned out across every variant carrying that vendor, then SKU-level values
written on top so specific settings are not overwritten by general ones.
`Re-stock Times` sits on the same screen and has no counterpart in Shopify's
recommended schema — it needs a metafield of its own.
### Pagination
Confirmed on a live store, 8 Aug 2026, after importing 100 products:
`?page=N` works, the page size is **30 rows**, and the nav is Polaris markup in a
`.pagination-row` container. The `last` control links directly to the final page,
so the total is readable in one request:
```html