REST API
The SKUHelm API
Plain JSON over HTTPS. Base address https://www.skuhelm.com/api/v1. There's also an OpenAPI document.
Keys
Make a key in the console under Settings, API and agents. A key belongs to one store and is shown once. Send it as Authorization: Bearer cs_live_.... Revoke it from the same page and it stops working at once.
/api/v1/catalog/validateValidate items
Check up to 1000 feed rows against OpenAI's spec. With a key, your store settings fill gaps first, exactly as your live feed would.
- Auth
- Key optional
- Limit
- Without a key: 30 a minute per IP address
itemsbody | object[] | Feed rows. A bare array as the whole body works too. |
apply_store_settingsbody | boolean? | With a key, fill gaps from your store settings first. Default true. |
curl -X POST https://www.skuhelm.com/api/v1/catalog/validate \
-H "content-type: application/json" \
-d '{"items":[{
"item_id": "MUG-01",
"title": "Speckled stoneware mug",
"price": "24.00 GBP",
"availability": "in_stock"
}]}'{
"ok": true,
"spec_version": "openai-feed-2026-10",
"summary": { "total": 1, "valid": 0, "blocked": 1, "checkoutReady": 0, "score": 0, "rules": [...] },
"items": [{
"item_id": "MUG-01",
"status": "blocked",
"issues": [{ "field": "brand", "code": "missing_brand", "severity": "error", "message": "Brand is missing." }]
}]
}/api/v1/catalog/syncStart a sync
Re-read every shop connected by address, record what changed and republish the feed. Uploaded files are skipped: upload a new file to update them.
- Auth
- API key
- Plan
- Pro
- Limit
- 6 every 10 minutes per store
curl -X POST https://www.skuhelm.com/api/v1/catalog/sync \
-H "authorization: Bearer $SKUHELM_KEY"{
"ok": true,
"synced": [{ "source_id": "src_...", "ok": true, "added": 2, "updated": 14, "removed": 0, "feed_version": 18 }]
}/api/v1/sync/statusRead sync status
Item counts by status, the live feed version and its addresses, each source and the last five syncs.
- Auth
- API key
curl https://www.skuhelm.com/api/v1/sync/status \
-H "authorization: Bearer $SKUHELM_KEY"{
"ok": true,
"store": { "slug": "your-shop", "name": "Your Shop", "plan": "pro" },
"items": { "total": 412, "ready": 380, "no_checkout": 20, "blocked": 12, "left_out": 0, "gone_from_shop": 3 },
"feed": { "version": 18, "published_at": "...", "item_count": 400, "checkout_ready": 380, "held_back_by_plan": 0, "urls": { ... } },
"sources": [...],
"recent_syncs": [...],
"sync_schedule": "every 60 minutes"
}/api/v1/feeds/{store}/acp.jsonFetch a published feed
The live feed as one JSON document with its version and counts. Public, like the feed files themselves.
- Auth
- Public
storepath | string | The store's slug, shown in the console under Feed. |
curl https://www.skuhelm.com/api/v1/feeds/your-shop/acp.json{
"ok": true,
"store": { "slug": "your-shop", "name": "Your Shop" },
"version": 18,
"item_count": 400,
"files": { "json": "...", "jsonl": "...", "csv": "..." },
"items": [{ "item_id": "MUG-01", "title": "Speckled stoneware mug", ... }]
}Feed files
The feed itself, as OpenAI reads it. Public, cached for a minute, with an ETag so an unchanged feed answers 304.
- GET /feeds/{store}.jsonl
- GET /feeds/{store}.csv
Errors
Every error has the same shape, so code can branch on error.code without reading the message.
{ "ok": false, "error": { "code": "unauthorized", "message": "...", "hint": "...", "docs": "/docs/api#errors" } }| 400 | bad_request | The body or a parameter is wrong. The message says which. |
| 401 | unauthorized | No API key, or the key was revoked. |
| 402 | payment_required | That needs the Pro plan. |
| 404 | not_found | No such store, or it hasn't published a feed yet. |
| 422 | source_failed | We couldn't read the shop. The message says what it returned. |
| 429 | rate_limited | Too many calls. Wait the number of seconds in Retry-After. |
| 500 | internal | Our fault. It has been reported. |
| 503 | unavailable | The database is briefly unreachable. Try again shortly. |