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.
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
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.
| Key | Prefix | Reach |
|---|---|---|
| Store secret key | pc_sk_ | Every product in the catalog, vendor-owned ones included. It works on every push route. |
| Vendor key | pc_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.
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.
Open a job
POST /v1/catalog/jobs answers a job id. A job is a staged, all-or-nothing push.
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.
Close the job
Closing applies every staged batch in the order it arrived, then runs the full sync if you asked for one.
Follow it
Poll the job, wait for the completion webhook, and download the error report for the rows that failed.
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):
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
| Page | What it covers |
|---|---|
| First vendor feed in 10 minutes | Start here: a working feed, step by step, with expected answers. |
| Concepts | Keys, the job lifecycle, merge and replace, fair turns, ownership and holds. |
| Limits and throughput | Every limit, and two measured production runs. |
| Troubleshooting | Symptom, cause, fix. |
| Reference for AI agents | How an agent runs a full sync; links the OpenAPI document. |
| Vendors and keys | Ownership, key reach, SKU clashes, adoption, holds, the vendor winning, vendor on orders. |
| Jobs, batches and runs | Merge and replace, staged jobs, limits, full sync, fair turns, reports. |
| Products | Read, archive, restore, rename, the 30-day purge, categories. |
| Stock | Set and adjust, idempotency, per-row results, stock in a job. |
| Webhooks | Completion events, signatures and the retry schedule. |
| Errors | The envelope and every error code. |
| Admin SDK | The experimental @usethrottle/catalog/admin entry. |
| API reference | Every operation, rendered from the OpenAPI document. |
Known limits
None of these is a bug; plan around them.
- Stock in a job applies at close. Use
PATCH /v1/catalog/inventoryfor 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.