Product Catalog · Push API

Catalog Push API

The push API is how a vendor's own system, or the merchant's, puts products into the Product Catalog: create, update, rename and archive them by the vendor's own product id, at any size, with a result for every row. It lives next to the storefront API, on the same host.

Server-side only
Push keys are secret. Never put a pc_sk_ or pc_vk_ key in a browser, a mobile app or a repository. The storefront API takes publishable keys; this one does not.

What it is

Base URL
https://catalog.usethrottle.dev/v1/catalog

Every route takes the key as a bearer token (Authorization: Bearer pc_…). Answers are JSON, and every error answers the same envelope (see Errors). The OpenAPI document is public at GET /v1/catalog/openapi.json.

KeyPrefixReach
Store secret keypc_sk_Every product in the catalog, vendor-owned ones included. It works on every push route.
Vendor keypc_vk_Only that vendor’s own products, jobs and runs. Another vendor’s or the store’s resource answers 404, and a store-only route 403 VENDOR_KEY_NOT_ALLOWED. A paused vendor’s keys answer 403 VENDOR_PAUSED.

Callers address a product by their own product id, externalId, which is unique per vendor. The URL handle is an ordinary field you can change. Vendors and keys covers who owns what.

How a push works

Pushing is async only. A push answers 202 with a job or run id and is applied in the background, so a feed of a hundred thousand products does not hold a request open. The one synchronous route is PATCH /v1/catalog/inventory, for live stock.

1

Get a key

The merchant uses a store secret key (pc_sk_), or adds a vendor in the dashboard under Storefront, Vendors and creates a vendor key (pc_vk_) for it. The key is shown once.

2

Open a job

POST /v1/catalog/jobs answers a job id. A job is a staged, all-or-nothing push.

3

Send batches

POST up to 1,000 rows and 5 MiB per batch to the job. Each batch is checked when it arrives; nothing is applied yet.

4

Close the job

Closing applies every staged batch in the order it arrived, then runs the full sync if you asked for one.

5

Follow it

Poll the job, wait for the completion webhook, and download the error report for the rows that failed.

Open a job (the first of five calls)
curl -X POST https://catalog.usethrottle.dev/v1/catalog/jobs \
  -H "Authorization: Bearer $CATALOG_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "merge", "fullSync": false }'

A job is staged: nothing applies until it is closed. A job that is not closed within 24 hours expires, its staged batches are discarded and never applied, and it ends as failed (JOB_EXPIRED). Send the feed again in a new job.

In TypeScript, jobs.run does all five steps for you (admin SDK):

A feed as one job
import { createCatalogAdmin } from '@usethrottle/catalog/admin';
import type { PushRow } from '@usethrottle/catalog/admin';

const admin = createCatalogAdmin({ key: process.env.CATALOG_ADMIN_KEY ?? '' });

// The rows of one product must be next to each other, in one batch.
const rows: PushRow[] = [
  {
    externalId: 'hr-1001',
    productHandle: 'ethiopia-guji',
    sku: 'HR-GUJI-250',
    status: 'active',
    product: { title: 'Ethiopia Guji', brand: 'Highland Roasters' },
    variant: { priceMinor: 1850, currency: 'USD', quantity: 40 },
  },
];

// Open a job, stage the batches, close it: nothing applies before the close.
const job = await admin.jobs.run(rows, { mode: 'merge' });

// wait() resolves when the job has finished or expired. A job that failed
// still resolves: read failureCode.
const done = await job.wait();
if (done.failureCode !== null) {
  console.error(done.failureCode);
  console.error(await job.errorsCsv());
} else {
  console.log(done.created, done.updated, done.unchanged, done.rowsFailed);
}

Tax codes and quotes

A row's product.taxCode sets the product's tax code, such as Avalara's PF050101. There is one per product, shared by all its variants. It must be upper case and match ^[A-Z0-9]{1,25}$ (file imports trim and upper-case it for you), and a sent null removes it. Vendor pushes and product reads carry it too. Storefront answers, feeds and JSON-LD never include it.

Pushed products are what Throttle's quotes look up. When a rep picks a product for a quote line, or an API caller sends a line with only its SKU, Throttle asks the catalog through a signed, server-to-server quote lookup. The lookup answers the variant's name, description, image, price in the quote's currency (the customer's price-group price when there is one), tax code, stock status and quantity limits. Out-of-stock variants are still returned; draft and archived products are not. No push key, publishable key or browser can call the lookup. See Catalog lines in the quotes guide.

In this section

PageWhat it covers
First vendor feed in 10 minutesStart here: a working feed, step by step, with expected answers.
ConceptsKeys, the job lifecycle, merge and replace, fair turns, ownership and holds.
Limits and throughputEvery limit, and two measured production runs.
TroubleshootingSymptom, cause, fix.
Reference for AI agentsHow an agent runs a full sync; links the OpenAPI document.
Vendors and keysOwnership, key reach, SKU clashes, adoption, holds, the vendor winning, vendor on orders.
Jobs, batches and runsMerge and replace, staged jobs, limits, full sync, fair turns, reports.
ProductsRead, archive, restore, rename, the 30-day purge, categories.
StockSet and adjust, idempotency, per-row results, stock in a job.
WebhooksCompletion events, signatures and the retry schedule.
ErrorsThe envelope and every error code.
Admin SDKThe experimental @usethrottle/catalog/admin entry.
API referenceEvery operation, rendered from the OpenAPI document.

Known limits

None of these is a bug; plan around them.

Image copying is a background job
Image copies run in two phases. While a job applies, they run alongside it: about 200 to 285 copies per minute at peak in one production run (2026-11-01, Worker e2f3c98a, a single run on one store). Copies still waiting when the job ends are picked up by a sweep that runs every 5 minutes and publishes up to 100 jobs per run, about 20 copies per minute (in the first run, 1,607 leftover copies took about 40 minutes). Until a copy lands, the storefront serves the vendor's own image URL, so keep those URLs public and stable. A catalog also has a budget of 20,000 image resizes per calendar month (roughly 6,700 images), so a feed with more images than that stays partly on vendor URLs for longer.
  • Stock in a job applies at close. Use PATCH /v1/catalog/inventory for live stock (Stock).
  • The legacy checkout records no vendor on an order line. With the Vendor on orders setting on, only carts priced by the catalog's line resolver record metadata.lineResolver.vendorId (Vendor on orders).
  • Vendor batches are canonical rows. A mapping profile cannot set externalId.
  • Job CSV reports above 900 parts answer 409 REPORT_TOO_LARGE.
  • The admin SDK entry is experimental. A breaking change to it ships as a minor release.

Categories now go up to 10 levels deep everywhere the tree is used; the storefront's categories.tree takes a depth of 1 to 10.