API reference
Send your catalogue to omniSKU, have it checked and fixed for each channel, and pull the clean feeds back. JSON over HTTPS, versioned under /api/v1.
Getting started
Create a token under Settings → API access, then send a catalogue and ask for a channel to be checked. Scanning costs 1 credit per new SKU, exactly as in the app; re-checking the same SKUs is included. AI fixes are subject to fair use: they are capped in proportion to what the account has paid, and message says when they were paused.
# 1. Create a catalogue and scan it for Google Shopping
curl -X POST https://omnisku.com/api/v1/catalogs \
-H "Authorization: Bearer $OMNISKU_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "Spring range", "channels": ["google_shopping"],
"products": [{"sku": "TEE-1", "title": "Classic Cotton Tee", "price": "19.50"}]}'
# → 202 Accepted, status "scanning"
# 2. Poll (or use a webhook) until status is "ready"
curl -H "Authorization: Bearer $OMNISKU_TOKEN" https://omnisku.com/api/v1/catalogs/42
# 3. Download the cleaned feed
curl -H "Authorization: Bearer $OMNISKU_TOKEN" \
"https://omnisku.com/api/v1/catalogs/42/channels/google_shopping/feed?format=xml"
Authentication
Send your token in an Authorization: Bearer osk_… header on every request. Tokens are shown once when created and stored only as a hash, so keep them in a secret manager. A token is either read-only or read + write; revoke one at any time in Settings.
Tokens act as you: they only ever see your own catalogues.
| Endpoint | Description |
|---|---|
GET/api/v1/me | Your email, credit balance, token scope, and the channels and formats available. |
Formats
Accepted on the way in
- JSON: a
productsarray of objects. Nested objects flatten toshipping_price-style keys; arrays are comma-joined. - File upload (multipart, field
file): CSV, TSV, XML or JSON. - XML: Google Shopping RSS 2.0 (
g:namespace), Atom, or any XML with a repeating product element. Namespace prefixes are dropped (<g:id>becomesid), nested elements flatten (shipping_price), repeated elements are comma-joined, and attributes become columns. DOCTYPE and entity declarations are rejected.
Columns are matched to omniSKU's product fields automatically (id, g:image_link, Variant SKU…). Pass a mapping to override, for example {"Product Code": "sku"}. Every catalogue needs a SKU column. Canonical fields: sku, group_id, title, description, brand, price, sale_price, currency, quantity, availability, condition, link, image_link, additional_images, gtin, mpn, category, color, size, material, gender, age_group, tags, bullet_points.
Available on the way out
| Channel | Key | Formats |
|---|---|---|
| Google Shopping | google_shopping | tsv, xml, json |
| Meta Catalog | meta_catalog | csv, tsv, xml, json |
| Amazon | amazon | csv, tsv, json |
| Etsy | etsy | csv, json |
| Agent readability | ai_agents | csv |
Catalogues
| Endpoint | Description |
|---|---|
GET/api/v1/catalogs | List your catalogues. Paginated: page, per_page (max 200). |
POST/api/v1/catalogs | Create a catalogue from products (JSON) or a file. Body: name (required), channels, mapping, scan (default true when channels are given), and instead of products you can send a file or a feed_url (a public https link we fetch for you; add refresh as daily or weekly to re-fetch it on a schedule, syncing by SKU and re-scanning). Returns 202 when scanning started, 201 otherwise, 402 if you lack credits (the catalogue is kept). |
GET/api/v1/catalogs/:id | Status (mapped, scanning, fixing, ready), per-channel stats, scan_cost (credits a scan would charge now), and feed URLs once ready. |
POST/api/v1/catalogs/:id/scan | (Re)scan. Optional channels. Asynchronous: returns 202. |
DELETE/api/v1/catalogs/:id | Delete the catalogue and its data. |
Products & sync
| Endpoint | Description |
|---|---|
GET/api/v1/catalogs/:id/products | Products with their current (fixed) data and issues. Filters: sku, channel, severity, code. |
POST/api/v1/catalogs/:id/products | Upsert by SKU. {"products": [...], "scan": true}. Matching SKUs are updated in place, new SKUs are appended; up to 10,000 per request. Already-scanned SKUs aren't charged again. |
Issues & fixes
| Endpoint | Description |
|---|---|
GET/api/v1/catalogs/:id/issues | Filters: channel, severity (error blocks a listing, warning is advice), code, field. |
POST/api/v1/catalogs/:id/fixes | Run one-click fixes in the background. Optional channel, code, field to narrow it. Fixes are logged per product and reversible in the app. |
Feeds
| Endpoint | Description |
|---|---|
GET/api/v1/catalogs/:id/channels/:channel/feed | ?format=json|csv|tsv|xml (default json; formats are per channel, otherwise 406). Add only_valid=1 to leave out products that still have errors. |
XML feeds are RSS 2.0 with the g: namespace, the shape Google Merchant Center and Meta Commerce Manager read.
Hosted feed URLs
For a channel that fetches a feed on a schedule, use the hosted URL from the catalogue response (or from the channel page in the app): /feeds/<token>/google_shopping.xml. It needs no header, so keep it private; regenerate it from the app to revoke the old one. It always serves the latest fixed data.
omniSKU doesn't push to Google, Meta, Amazon or Etsy for you yet. You pull the feed, or point the channel at the hosted URL.
Webhooks
Add endpoints in Settings or with the API. Each delivery is a POST with a JSON body {id, event, created_at, data}, retried with back-off on failure, and an endpoint that keeps failing is paused. Webhook URLs must be public https addresses.
| Event | When |
|---|---|
scan.completed | A scan (including automatic fixes) finished. data.catalog carries the new stats. |
scan.failed | A scan failed. Credits for it are refunded. |
fixes.completed | A fix run finished. |
| Endpoint | Description |
|---|---|
GET/api/v1/webhooks | List. |
POST/api/v1/webhooks | {"url": "https://…", "events": ["scan.completed"]}. The signing secret is returned once. |
DELETE/api/v1/webhooks/:id | Remove. |
Verifying a delivery
Each request carries X-OmniSKU-Timestamp and X-OmniSKU-Signature. The signature is sha256= plus the hex HMAC-SHA256 of "{timestamp}.{raw body}" using your secret. Compare in constant time and reject old timestamps.
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
ok = ActiveSupport::SecurityUtils.secure_compare(expected, signature_header) &&
(Time.now.to_i - timestamp.to_i).abs < 300
MCP server
omniSKU is also a remote Model Context Protocol server, so an AI assistant can do the work for you: upload a catalogue, check it, fix it and hand back the feed. It speaks Streamable HTTP at:
https://omnisku.com/mcp
Add it as a custom connector (or remote MCP server) in an assistant that supports them, and approve access when asked. There is nothing to install and no token to copy. In Claude Code:
claude mcp add --transport http omnisku https://omnisku.com/mcp
Clients that can't do OAuth can send a Settings token instead: Authorization: Bearer osk_….
| Tool | What it does |
|---|---|
get_account | Credits, access level, channels and formats. |
list_catalogs, get_catalog | Status, per-channel counts, the credit cost of a scan, and feed links. wait_seconds waits for a running scan or fix. |
create_catalog write | From products, inline content (CSV, XML or JSON) or a source_url. Uses no credits. |
scan_catalog write | Checks products against the chosen channels. Costs credits, so it requires confirmed_cost to equal the current cost: the assistant must read the cost and get your agreement first. |
summarize_issues | Problems grouped by channel, field and code, with counts, an example and example SKUs. Says whether each is fixable. |
fix_catalog write | Applies rule and AI fixes. No extra credits (AI fixes are subject to fair use; very large catalogues may get their AI fixes a little later, and message says so); every change is logged and reversible in the app. |
list_products, update_products write | Read products and open issues; upsert up to 500 products by SKU. |
get_feed | A private download link, formats and a preview for a channel's cleaned feed. |
There is also an optimise_catalog prompt that walks through the whole flow. Choosing Read only on the approval screen hides every write tool. Results are summarised and truncated so large catalogues don't overwhelm the assistant, and product text is marked as data, not instructions. The server has no delete tool.
OAuth
The MCP server uses OAuth 2.1 with PKCE (S256, required) and public clients. Compliant clients discover everything themselves:
| Endpoint | |
|---|---|
/.well-known/oauth-protected-resource | Resource metadata (RFC 9728). An unauthenticated /mcp call returns 401 with a WWW-Authenticate header pointing here. |
/.well-known/oauth-authorization-server | Server metadata (RFC 8414). |
POST/oauth/register | Dynamic client registration (RFC 7591). Redirect URIs must be https, a loopback address, or a custom app scheme. |
/oauth/authorize | Consent screen. The user chooses read-only or read + write. |
POST/oauth/token | authorization_code and refresh_token grants. Access tokens last 1 hour; refresh tokens last 30 days and rotate on use. |
POST/oauth/revoke | Revocation (RFC 7009). Users can also disconnect an app under Settings → API access. |
Access tokens issued this way also work on the REST API above.
Errors & limits
Errors are JSON: {"error": {"code": "…", "message": "…"}}.
| Status | Meaning |
|---|---|
401 | Missing, invalid or revoked token. |
402 | insufficient_credits, with needed and available. |
403 | Read-only token used for a write. |
404 | Not found, or not yours. |
406 | That channel doesn't offer the requested format. |
409 | The catalogue is being scanned or fixed. Retry shortly. |
422 | Invalid input: unreadable file, no SKU column, duplicate SKUs… |
429 | Rate limited (300 requests per minute per token). |
Catalogues are capped at 50,000 rows and uploads at 100 MB.