FreshStore Store API: endpoint reference for building your own tools

This is your store's own API, not the Amazon Creators API. Use it to build your own tools, dashboards, scripts and integrations on top of your FreshStore store. The Amazon Creators API is Amazon's separate product data service, which FreshStore already handles for you. If you have not created a key yet, see the guide on creating an API key for your store.

Every store has an API that can be used to get data from or update your store. That includes products, offers, categories, articles, pages, settings, Auto Populators and analytics. Using an API needs some technical knowledge.

Base URL: https://{your-store-domain}/api

Each store has its own API, its own keys and its own rate limit. A key works on one store only.

Last verified against routes/api.php: 2026-10-15.

Changelog

  • 2026-10-15 (retire and CDN). POST /api/store/clear-cache now also purges the store's CDN copy, unless you send edge: false; the reply says whether the purge was queued. New: POST /api/redirects/bulk (up to 500 redirects in one call, one CDN purge), POST /api/store/retire-catalogue (switch off a whole catalogue and redirect every old URL in one call, dry run first) and GET /api/store/retire-catalogue/{id} (its result).
  • 2026-10-15 (sections, texts and design). New: GET and PATCH /api/pages/{id}/sections and /api/zones/{page_type}/{zone}/sections (the Sections Design > Editor arranges: read them with the blocks you can add, then update, show, hide, move, add and remove in one call, published at once), GET and PATCH /api/store/texts (every template text, Custom CSS included), GET and PATCH /api/store/design (colour scheme, custom palette and fonts), POST /api/store/design/palette (a palette from five colours, or from the AI, not saved) and POST /api/store/featured-products (choose the homepage Featured Products).
  • 2026-10-15 (images and template). New: POST /api/products/{id}/images (a gallery picture from a URL), POST /api/categories/{id}/images (the listing image or banner), POST /api/store/images (the logo, store icon, link preview picture, homepage hero, About page picture and the person photo, stored where the Design pages store them), GET /api/store/template (the current template, every template with its readiness for this store, and a dry run) and POST /api/store/template (the template switch, with the same guards as Design > Templates). template.frontend in settings stays read only; switch with the new endpoint.
  • 2026-10-15 (articles). POST /api/articles with a future published_at schedules the article: saved disabled, written at once when generate_ai_content is true, live on its date with that date kept. A scheduled article now goes live through the full save path, so it reaches the sitemap, the CDN purge and IndexNow on its date.
  • 2026-10-15. GET /api/products takes own_touch=with|without: products with or without something of the member's own (a verdict, pros or cons text, or an uploaded photo). The same rule as the "Your own touch" filter on the Products list and the thin pages count. Any other value returns 422.
  • 2026-10-13. Author ratings: products take author_rating (1 to 5 in steps of 0.5), author_verdict, author_pros and author_cons, and return author_reviewed_at; articles take the same plus reviewed_product_id, and return formula. GET /api/products sorts by author_rating. New settings group schema (what the store sends to Google as structured data). A store that has not finished its upgrade answers 409 to a rating field; retry in a minute.
  • 2026-10-08. POST and PATCH /api/offers: merchant must be one of amazon, ebay, etsy, walmart, aliexpress or custom (case-insensitive), otherwise 422. Before, any string was stored and the offer could never refresh.
  • 2026-10-07 (later). New: POST /api/store/rebuild-sitemap, POST /api/articles/{id}/images and GET /api/images/stock, and Auto Populator endpoints (GET /api/populators, GET and PATCH /api/populators/{id}, POST /api/populators/bulk). Writing xml-sitemap settings now queues a sitemap rebuild within about ten minutes. Tightened: /tools now needs the store scope, /redirects the settings scope; sort_by on GET /api/products must be one of the listed columns (422, not 500); summary length is capped per period; POST /api/member-facts is platform only; a member key cannot change template.frontend.
  • 2026-10-07. Full audit against routes/api.php and every action class. Documented 24 live endpoints that were missing (23 absent, plus categories/fill, which had only a table row): store/instant-store-readiness, store/indexnow, store/messages/summary, store/clear-cache, store/footer-credit, tools/offer-health, the three redirects routes, users/invite, users/{id}/invite, DELETE users/{id}, the three launch-plan routes, recommendations, member-facts, recompute, products/import-links, categories/fill, brands/clean, analytics/sources/summary, analytics/clicks/by-merchant, analytics/clicks/top-products. Fixed: error bodies (auth failures are 403 {"error":"Forbidden"}, not 401), token ability rules, the User-Agent block, POST /api/offers/{id}/products (adds, never removes), POST /api/store/clean defaults, generate-topics cost and its niche field, store stats fields, user fields, settings write responses. Added the settings group field lists, the "What the API cannot do yet" section, and the categories note on PATCH /api/products/{id}.

Getting Started

1. Create an API key

A store admin creates keys in that store's admin area: Settings > API Keys > Create API Key. Give the key a name, tick the permissions it needs, and optionally set an expiry date. The two "AI assistant" presets tick a sensible set and expire after 90 days.

You see the key once. Copy it and store it safely. It cannot be shown again.

Keys are per store. There is no account-level key that works across all your stores, and keys cannot be created through the API.

2. Authenticate requests

Send the key as a Bearer token on every request:

Authorization: Bearer YOUR_API_KEY

Responses are always JSON. You do not need to send an Accept header.

3. User-Agent and the CDN

Stores sit behind a CDN that blocks common HTTP-library User-Agents (python-requests, python-httpx, curl/, axios/, node-fetch/, Go-http-client/) on store pages. From 2026-10-07, requests to /api/... are exempt from that block, so the defaults of Python requests and httpx, curl, axios, node-fetch and Go reach your store. A blocked request gets 403 with an HTML "403 Forbidden" page, not JSON. If you see that page on an API call, set your own User-Agent, for example MyStoreScript/1.0.

User-Agent: MyStoreScript/1.0

The store itself rejects an empty or library User-Agent on a request that carries no API key (for example the public status endpoint). Requests that carry a key are not User-Agent filtered by the store.

4. Permissions (token abilities)

A key holds one or more abilities. The checks run in this order:

  1. * (Full Access) skips every check below.
  2. Read or write. GET needs read or write. POST, PATCH, PUT and DELETE need write. A key with only per-resource abilities (for example only products:write) fails this step. Tick the broad Read or Write box as well.
  3. Per-resource scope. If the key holds any resource:verb ability, every request must also match a resource ability for its path: {resource}:read for GET, {resource}:write for the others, or {resource}:*. If the key holds no resource:verb abilities, this step is skipped and the broad read / write covers every resource.

The first path segment after /api/ decides the resource:

Path starts with Resource ability needed (scoped keys)
/products products
/offers offers
/categories categories
/brands brands
/articles articles
/article-categories article-categories
/pages, /zones pages
/analytics analytics
/settings, /launch-plan settings
/store, /recommendations, /member-facts, /tools store
/redirects settings
/populators categories
/images articles
/users users
/data data
/recompute both settings:write and store:read

Notes:

  • The admin screen offers store:read but no store:write, and no data:write or analytics:write. So a scoped key cannot call the POST /api/store/... endpoints. Use a key with Full Access, or with the broad Write box and no per-resource boxes.
  • Some user endpoints also check who owns the key, not only its abilities. See Users.
  • A request that fails these checks gets 403 with a message naming the missing access, for example {"message": "This token does not have access to products."}.

5. Rate limiting

60 requests per minute, per store, per key. Each key on each store has its own bucket. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining headers. Over the limit you get 429 with {"error": "Too Many Requests"}. Wait for the minute to roll over and retry.


Response Format

Success

Most responses wrap the result in a data key:

{
  "data": { ... }
}

Exceptions are called out on each endpoint (for example GET /api/store/instant-store-readiness returns the object without a data wrapper, and the settings writes return a message).

Errors

Status When Body
401 The key's user lacks API access "Unauthorized." (a JSON string)
402 An AI endpoint and the store has no AI chips left {"error": "not_enough_chips", "message": "..."}
403 Missing, invalid or expired key {"error": "Forbidden"}
403 Key lacks the ability (see Permissions) {"message": "This token does not have write access."}
403 The key's owner may not manage that user, or a platform-only route {"error": "Forbidden"}
403 Blocked User-Agent at the CDN (store pages, not /api) HTML "403 Forbidden" page
403 Library User-Agent on an API call with no key Empty body
404 Unknown route, unknown id, unknown launch plan key {"error": "Not Found"}
422 Validation failed {"message": "...", "errors": {"field": ["..."]}}
422 A business rule refused the request {"message": "..."}
422 Some settings could not be saved {"message": "Some settings could not be saved.", "failed": ["key"]}
429 Rate limit {"error": "Too Many Requests"}
500 Server error {"error": "Server Error"}
503 The store is still being built (install window) {"error": "Service Unavailable", "reason": "installing"} with Retry-After: 30
503 An AI call failed {"error": "ai_generation_failed", "message": "..."}

A bad or missing key returns 403, not 401.

Pagination

List endpoints are paginated and accept:

Parameter Type Default Max Description
page integer 1 Page number
per_page integer 15 100 Items per page

They return Laravel's standard envelope:

{
  "data": [ ... ],
  "links": { "first": "...?page=1", "last": "...?page=10", "prev": null, "next": "...?page=2" },
  "meta": { "current_page": 1, "from": 1, "last_page": 10, "per_page": 15, "to": 15, "total": 150, "path": "...", "links": [ ... ] }
}

GET /api/redirects is different: default per_page is 50, and it returns only meta.total, meta.per_page and meta.current_page.

Booleans

Write endpoints for products, offers, categories, articles and pages accept booleans as JSON true / false or as the strings "true", "false", "1", "0".


What the API cannot do yet

Checked against the code on 2026-10-07. None of these have an endpoint:

  • Take an uploaded file. Every image endpoint takes the URL of an image: Set Article Image, Set Product Image, Set Category Image and Set Store Image. Find free stock photos with Search Stock Images. Images for other pages and for the offer box background are made with generate_images (see Image source).
  • Create or delete an Auto Populator, or run one now. You can list, switch on or off and edit existing ones (see Auto Populators). POST /api/categories/fill is a one-off fill, not a populator.
  • Create API keys. Keys are created by a store admin in the admin area, one store at a time.
  • Account-level keys. There is no key that works across all the stores on an account.
  • Set a video in a section, or a picture by upload. Section images take a URL; videos are set in Design > Editor.
  • Edit a page in a Country path's language. Section and text writes change the main store. Translations are edited in the admin.

Store

Get Status (Public)

GET /api/store/status

No key needed. Send a non-library User-Agent (see Getting Started). Answers during an install too.

{ "data": { "up": true } }

When the store cannot serve pages: {"data": {"up": false, "reason": "installing"}}. Reasons: no_tenant, not_installed, deleted, suspended, installing, no_template.


Get Store Info

GET /api/store/info

Ability: store:read.

{
  "data": {
    "name": "My Store",
    "url": "https://store.example.com",
    "niche": "tech",
    "live": true,
    "default_country": "US",
    "enabled_countries": ["US", "GB", "DE"],
    "version": "3.2.0",
    "cart_enabled": false,
    "advanced_mode": true
  }
}

Get Store Stats

GET /api/store/stats

Ability: store:read. Also at GET /api/data/stats (ability data:read).

Parameter Type Description
images boolean 1 adds offers_images_pending and articles_images_pending (slower)
{
  "data": {
    "products": 150,
    "categories": 23,
    "articles": 45,
    "articles_generated": 30,
    "brands": 12,
    "offers": 320,
    "messages": 4,
    "offers_expired": 3,
    "offers_out_of_stock": 7,
    "products_without_offers": 2
  }
}

Keys come back sorted by value, smallest first.


Get Instant Store Readiness

GET /api/store/instant-store-readiness

Ability: store:read. Content counts used to judge whether a store is ready to be copied. Not wrapped in data.

{
  "product_count": 150,
  "category_count": 23,
  "article_count": 45,
  "empty_leaf_category_count": 0,
  "homepage_customised_fields": 12,
  "homepage_hero_is_custom": true,
  "locked_content_count": 0,
  "unfinished_article_generation_count": 0,
  "last_content_change_at": "2026-10-06 14:02:11"
}

Get Store Warnings

GET /api/store/warnings

Ability: store:read. The same warnings as the admin's Store Health page.

{
  "data": [
    {
      "message": "To get paid for your Amazon sales you need to add an Amazon Affiliate Tag",
      "action": "Find Out More",
      "url": "https://store.example.com/admin/store-health"
    }
  ]
}

Get Store Locales

GET /api/store/locales

Ability: store:read. The enabled country configurations, keyed by lowercase country code.

{
  "data": {
    "us": { "name": "United States", "iso_3166_2": "US", "...": "..." },
    "gb": { "name": "United Kingdom", "iso_3166_2": "GB", "...": "..." }
  }
}

Get IndexNow Status

GET /api/store/indexnow

Ability: store:read. Whether automatic IndexNow submission is working. Never returns the IndexNow key.

Parameter Type Default Description
recent integer 10 Recent attempts to return, 1 to 50
{
  "data": {
    "summary": {
      "last_submitted_at": "2026-10-07T09:12:00+00:00",
      "success_24h": 4,
      "success_30d": 61,
      "error_30d": 1,
      "total_30d": 62
    },
    "recent": [
      {
        "url": "https://store.example.com/products/example",
        "submitted_at": "2026-10-07T09:12:00+00:00",
        "status": "success",
        "response_code": 200,
        "error_message": null
      }
    ]
  }
}

status is one of success, error, spam, exception.


Contact Messages Summary

GET /api/store/messages/summary

Ability: store:read. Contact-form messages over time. Same shape as the clicks and checkouts summaries.

Parameter Type Default Description
period string day day, week, or month
length integer 30 Number of periods to return. At most 366 days, 260 weeks or 120 months; more returns 422
{
  "data": {
    "today": { "messages": 1 },
    "this_month": { "messages": 6 },
    "total": { "messages": 9 },
    "periods": {
      "2026-10-06": { "messages": 0 },
      "2026-10-07": { "messages": 1 }
    }
  }
}

total covers the requested window only. Week keys look like 2026-W41, month keys like 2026-10. Empty periods are included as zeros.


Clean Store

POST /api/store/clean

Ability: store:write (Full Access or broad write only, see Permissions). Deletes catalogue clutter. Counts of what it would delete are at GET /api/tools/offer-health.

Field Type Required Description
operations string[] No Any of expired_offers, out_of_stock_offers, offerless_products, empty_brands, empty_categories. Default: all except out_of_stock_offers. They always run in that order, whatever order you send.
{
  "data": {
    "deleted": { "expired_offers": 3, "offerless_products": 2, "empty_brands": 1, "empty_categories": 0 }
  }
}

Clear Store Cache

POST /api/store/clear-cache

Ability: store:write. Clears the store's own query cache, settings caches and global view data, so the next page view reads fresh data, and queues a purge of the store's whole CDN copy.

Field Type Required Description
edge boolean No Default true: also purge the CDN. Send false to clear only the store's own caches.

The CDN purge goes into the store's purge queue and usually reaches the CDN within a few minutes. It is skipped when the store has no CDN zone of its own. edge in the reply is queued or skipped, and edge_reason says why.

{
  "data": {
    "cleared": { "query_cache": true, "settings": true, "view_data": true },
    "edge": "queued",
    "edge_reason": "The whole CDN zone is queued for a purge, usually done within a few minutes."
  }
}

Rebuild XML Sitemap

POST /api/store/rebuild-sitemap

Ability: store:write. No body. The admin's XML Sitemap "Recreate" button: queues a fresh sitemap build and answers 202 at once. Calls while a build is waiting add nothing. The sitemap also rebuilds daily on its own, and within about ten minutes of an xml-sitemap settings change made through the API.

{ "data": { "queued": true, "last_generated_at": "2026-10-07T09:12:00+00:00" } }

Retire Catalogue

POST /api/store/retire-catalogue

Ability: store:write, products:write, categories:write, articles:write, brands:write and settings:write (it writes redirects). For a re-niche: switches off an old catalogue and redirects every old URL in one call. Nothing is deleted; every product, category, article and brand can be switched back on. A dry run unless dry_run is false.

Field Type Required Description
category_ids array One of three Those categories and every category under them. A product is in scope when every live category it sits in is in scope, so a product also in a kept category stays. Up to 5000 ids.
product_ids array One of three Those products. A category is in scope when all of its live products and live child categories are in scope. Up to 5000 ids.
created_before datetime One of three Everything made before this date: products, and categories whose live products and child categories are all in scope (a category that now holds a newer product stays).
include array No Any of products, categories, articles, brands. Default: all four.
redirect_to string No One path on this store, e.g. /categories/new-niche, where every retired URL goes.
redirect_map object No {category id or slug: path}. A retired URL goes to the entry for its category, or the nearest parent category with one, else to redirect_to. Up to 500 entries.
dry_run boolean No Default true.

Send exactly one of category_ids, product_ids or created_before.

Articles and brands follow what is retired. A brand is retired when it has at least one live product and every one of them is retired. An article is retired when it links to a live category (its product category) or a live product (the product it reviews) and every one of those is retired. With created_before, a brand with no live products and an article with no links are also retired when they were made before the date.

Redirects. Every retired URL gets a redirect to its target. A URL that already has a redirect keeps it (already_redirected). A URL with no target (no map entry and no redirect_to) gets none (no_target). After a live run the store clears its caches, purges its CDN copy once, rebuilds the sitemap and tells IndexNow about the retired URLs.

Refused (422): no scope or more than one; an unknown category or product id; a target that is not a path on this store, that does not answer, or that this same call retires; and a scope that would retire every product or every category with no redirect_to or redirect_map.

Dry run response: the counts and the first 50 URLs per type.

{
  "data": {
    "dry_run": true,
    "scope": "category_ids",
    "include": ["products", "categories", "articles", "brands"],
    "counts": { "products": 105, "categories": 6, "articles": 12, "brands": 9 },
    "redirects": { "to_write": 130, "already_redirected": 2, "no_target": 0 },
    "urls": {
      "products": [ { "from": "/products/old-swimsuit", "to": "/categories/new-niche", "redirect": "write" } ],
      "categories": [ { "from": "/categories/swimwear", "to": "/categories/beach-gear", "redirect": "write" } ],
      "articles": [],
      "brands": []
    }
  }
}

Live run response (200): the same counts and URLs, each with its result (written, already_redirected, no_target or failed), plus the redirect totals with every failure and its reason, and an id to read the result again later.

{
  "data": {
    "id": "3f0c2a8e-1b7d-4c55-9a51-0e7d2b9f4c11",
    "status": "done",
    "dry_run": false,
    "queued": false,
    "counts": { "products": 105, "categories": 6, "articles": 12, "brands": 9 },
    "redirects": { "written": 130, "already_redirected": 2, "no_target": 0, "failed": 0, "errors": [] },
    "urls": { "products": [ { "from": "/products/old-swimsuit", "to": "/categories/new-niche", "redirect": "written" } ] },
    "edge": "zone",
    "indexnow_queued": 128,
    "sitemap": "queued"
  }
}

A live run over 300 items is queued: the reply is 202 with status: queued, the planned counts and the id. Read the result with Get Catalogue Retire.

Get Catalogue Retire

GET /api/store/retire-catalogue/{id}

Same ability as Retire Catalogue. The result of a live retire, kept for a week. status is queued, running, done or failed (with error; a failed run changes nothing). An unknown or expired id returns 404.


Regenerate Stock Texts (AI)

POST /api/store/regenerate-stock-texts

Ability: store:write. Rewrites the template's generic stock copy (about page, contact box and similar) for your niche. Only texts that still hold the template default are rewritten; texts you have edited are left alone. Uses AI chips.

Field Type Required Description
niche string Yes Max 255 chars
brand_name string No Max 255 chars
keys string[] No Template text keys to rewrite. Default: all supported keys. Unknown keys are ignored.
{
  "data": {
    "regenerated": [{ "key": "ABOUT_PAGE_CONTENT_TITLE", "old": "...", "new": "..." }],
    "skipped": 3,
    "total_eligible": 5
  }
}

402 when out of chips, 503 when the AI call fails.


Pick Logo Icon (AI)

POST /api/store/pick-logo-icon

Ability: store:write. AI picks an icon for the template's text logo. Leaves an icon the store already chose alone.

Field Type Required Description
niche string Yes Max 255 chars
{ "data": { "status": "picked", "picked": "fa-camera", "label": "Camera" } }

status is one of picked, skipped (already set), no_option, no_choices, invalid_response.


Generate Store Logo (AI)

POST /api/store/generate-logo

Ability: store:write. Queues an AI logo for the store. Skips a store that already has its own logo.

Field Type Required Description
niche string Yes Max 255 chars
image_quality string No standard (default) or high. See Image quality.
{ "data": { "status": "queued" } }

Other answers: {"status": "skipped", "reason": "no_store_name"}, {"status": "skipped", "reason": "logo_already_set"}, {"status": "failed", "error": "..."}.


Set Store Image

POST /api/store/images

Ability: store:write. Sets one of the store's own pictures from an image URL. The store downloads the image itself and stores it where the Design pages store it, so the storefront shows it with no other step.

Field Type Required Description
collection string Yes Which picture. See the table below.
url string Yes A public http or https address of a JPEG, PNG, WebP or GIF, at most 10 MB. Redirects are not followed, so send the final address.
replace boolean No Default true: the new picture replaces the one there. false fills an empty slot and leaves a full one as it is.
collection What it is Same as
logo The header logo of the active template. Also sets the logo type to "image", because the header shows the picture only then. Design > Logo
icon The store icon (favicon), PNG only. The 180, 192 and 512 pixel sizes are made from it. Design > Store Icon
social The default link preview picture for pages without their own. Design > Global Images
hero The homepage hero picture, on every template. Pages > Homepage > Images
about The About page picture. Pages > About > Images
person The photo of the person behind the store. A square photo works best; it is shown as a circle. Design > About You
{ "data": { "collection": "logo", "url": "https://.../my-store-logo.png", "replaced": true, "added": true, "logo_type": "image" } }

replaced is true when a picture was there before. added is false when replace: false found the slot full; url is then the picture that stays. Extra fields: logo_type for logo; hero_section_image_cleared for hero, true when the homepage hero section named its own picture (or played a video) and that was cleared so the new picture shows; person_draft: false for person, which marks the person as yours, as saving About You does.

The logo, icon and link preview picture belong to the active template. A template switch carries them to the new template. The hero, About and person pictures belong to the store and stay through a switch.

A URL the store cannot use returns 422 with the reason in message. So does a store without the page the picture goes on.


Get Store Template

GET /api/store/template

Ability: store:read. The store's template, every template it can switch to with its readiness for this store, and with key a dry run of the switch. Reads only.

Parameter Type Required Description
key string No A template key. Adds target: what Switch Store Template would do. An unknown key returns 422.
{
  "data": {
    "current": { "key": "crystal", "name": "Crystal", "readiness": { "checks": [], "warnings": 0, "unmeasured": 0, "summary": { "ok": true, "text": "Ready for your store." } } },
    "templates": [
      { "key": "emporium", "name": "Emporium", "current": false, "early_access": false, "can_switch": true, "readiness": { "summary": { "ok": false, "text": "You have 40 products with a price. Emporium looks best with 100 or more." }, "warnings": 1, "unmeasured": 0 } }
    ],
    "switch_back": null,
    "target": {
      "key": "emporium",
      "name": "Emporium",
      "readiness": { "checks": [ { "ok": false, "text": "You have 40 products with a price. Emporium looks best with 100 or more.", "fix": null, "note": null, "count": 40, "min": 100, "missing": 60 } ], "warnings": 1, "unmeasured": 0, "summary": { "ok": false, "text": "You have 40 products with a price. Emporium looks best with 100 or more." } },
      "plan": { "status": "dry_run", "from": "crystal", "to": "emporium", "dry_run": true, "returning": false, "target_rows_ready": true, "custom_css_lines": 0, "pick_cards": 0, "content": { "options": 4, "texts": 12, "images": 1, "links": 3, "media": 0 }, "sections": { "created": 0, "mapped": 3, "unmatched": 1, "unmatched_blocks": { "testimonials": 1 } }, "stays": ["..."], "changes": ["..."], "next": [], "undo": "You can switch back at any time. Nothing is deleted." }
    }
  }
}

Readiness is the template's own checks held against this store, as the Templates gallery shows them: each check has ok, the sentence in text, and where the store can fill the gap a fix (write_guides, category_images, about_you). It warns and never blocks a switch. unmeasured counts checks the store could not measure; the summary then says to preview.

The examples are shortened. The plan is the switch's own dry run, the same answer the switch applies. stays, changes, next and undo are the lines the Design page's confirmation shows. A refusal comes back in its place, with the status listed under Switch Store Template. target_rows_ready: false means the template's rows are not on the store yet; the real switch adds them first. switch_back names the template the store came from, for a few days after a switch.


Switch Store Template

POST /api/store/template

Ability: store:write. Switches the storefront to another template and carries your content across. The same switch as Design > Templates, with the same rules.

Field Type Required Description
key string Yes The template key, one of the templates in Get Store Template. Anything else returns 422.
dry_run boolean No true answers what the switch would do, as target.plan in Get Store Template, and changes nothing.
bring_custom_css boolean No Default true. false leaves your Custom CSS on the old template.
{ "data": { "template": { "key": "emporium", "name": "Emporium" }, "readiness": { "checks": [], "warnings": 1, "unmeasured": 0, "summary": { "ok": false, "text": "..." } }, "status": "switched", "from": "crystal", "to": "emporium", "dry_run": false, "returning": false, "content": { "options": 4, "texts": 12, "images": 1, "links": 3, "media": 2 }, "sections": { "created": 7, "mapped": 3 }, "preset": "crystal", "preset_changed": false, "picks_seeded": 0, "message": "Your content came with you, and your visitors will see the new layout shortly. Switch back from this page at any time to get Crystal back exactly as it was." } }

readiness is measured before the switch. message is the sentence the Design page shows after a switch, naming any homepage band that stayed behind.

status HTTP Meaning
switched 200 Done.
dry_run 200 dry_run: true; nothing changed.
already_active 200 The store is on that template already. Nothing changed.
early_access_required 403 The template is in early access, and this store's account does not have it.
blocked_draft 409 The Store Editor holds unpublished changes. Publish or discard them first.
error 409 The store is finishing an update. Try again in a few minutes.
draft_template 422 The template is still being built.

What a switch does. Your products, categories, articles, pages and page addresses do not change. Texts, links, options and images you edited come across by key, and so do the logo and uploaded template pictures. Fonts stay with their template. Your Custom CSS comes across unless bring_custom_css is false. A theme the new template does not support falls back to its standard colours (preset_changed).

The homepage. The first time a store uses a template, the switch builds that template's homepage lineup and fills it with your homepage content, band by band (sections.mapped). Bands the new template has no place for stay on the old template, waiting (sections.unmatched_blocks). The lineup is built once: a store that comes back to a template it used before gets that homepage back as it left it (returning: true), and it does not gain bands a later release added to the template. Podium's pick cards are filled from your best rated products (picks_seeded).

Switching back is the same call with the old key. It restores the old template exactly, because a switch never writes the template it leaves. Nothing is deleted.


Get Template Texts

GET /api/store/texts

Ability: store:read. Every template text of the active template: the words the admin's Text pages edit (header bar, homepage boxes, About and Contact page texts, buttons and labels) and CUSTOM_CSS.

Parameter Type Description
keys string Comma-separated keys, e.g. ?keys=HEADER_BAR_TEXT,CUSTOM_CSS. Default: all.
{
  "data": {
    "template": "crystal",
    "custom_css_enabled": true,
    "new_design_enabled": true,
    "texts": [
      { "key": "HEADER_BAR_TEXT", "name": "Top Bar Text", "description": "...", "group": "Header", "page_key": null, "model": null, "type": "text", "text": "Free guides every week", "default": "...", "customised": true }
    ]
  }
}

text is what the store shows: your own text, or the default when you have not set one. customised is true when you have. text can be "" with customised: true: you chose to show nothing there.

Update Template Texts

PATCH /api/store/texts

Ability: store:write. Sets texts by key. The same save as the admin's Text pages and Design > Custom CSS.

Field Type Required Description
texts object Yes Key to text, up to 200 keys. null puts a text back to its default. "" shows nothing there.
{ "texts": { "HEADER_BAR_TEXT": "Free guides every week", "CUSTOM_CSS": "h1 { letter-spacing: -0.01em; }" } }

The answer is the Get Template Texts body for the keys you sent, plus changed (the keys whose stored value changed) and notes. A key whose value does not change is left alone, so sending the current text back changes nothing.

  • A key the template does not have refuses the whole request with 422 and an error per key. Nothing is written.
  • A text equal to its default is stored as "use the default", so a later change to the default reaches you.
  • CUSTOM_CSS is cleaned before it is saved: anything that would close the style element is removed, and it is cut at 50,000 characters. It is saved even while support has Custom CSS switched off on the store (custom_css_enabled: false); a note says it will show once it is back on. On Crystal it also needs the new design (new_design_enabled).
  • To rewrite the stock texts for your niche with AI, use Regenerate Stock Texts.

Get Design

GET /api/store/design

Ability: store:read. The colour scheme, the custom palette and the fonts of the active template, with every allowed value.

{
  "data": {
    "template": "crystal",
    "scheme": "skin-custom",
    "custom_palette": true,
    "new_design_enabled": true,
    "roles": { "accent": "#E8692C", "background": "#FFFFFF", "text": "#1F2328", "titles": "#2B3036", "buttons": "#B4451A" },
    "palette": { "COLOUR_ACCENT": "#E8692C", "COLOUR_LOGO": "#E8692C", "COLOUR_BG_DARK": "#E8692C", "COLOUR_BG_LIGHT": "#FFFFFF", "COLOUR_TITLE": "#2B3036", "COLOUR_SUBTITLE": "#2B3036", "COLOUR_TEXT": "#1F2328", "COLOUR_LIGHT": "#1F2328", "COLOUR_LINK": "#E8692C", "COLOUR_BUTTON_ACCENT": "#B4451A", "COLOUR_BUTTON_ACCENT_TEXT": "#FFFFFF", "COLOUR_BUTTON": "#FFFFFF", "COLOUR_BUTTON_TEXT": "#B4451A" },
    "fonts": { "TITLE_FONT": "nunito", "TITLE_FONT_WEIGHT": "bold", "TITLE_SIZE": "medium", "TITLE_ALIGNMENT": "center", "SUBTITLE_FONT": "rubik", "SUBTITLE_FONT_WEIGHT": "normal", "SUBTITLE_SIZE": "medium", "SUBTITLE_ALIGNMENT": "center", "BODY_FONT": "source-serif-4", "BODY_FONT_WEIGHT": "normal", "BODY_SIZE": "medium" },
    "choices": {
      "schemes": { "skin-indigo-go": "Indigo Go", "skin-blue-dream": "Blue Dream" },
      "fonts": { "TITLE_FONT": { "inter": "Inter", "outfit": "Outfit" }, "TITLE_FONT_WEIGHT": { "normal": "Normal", "bold": "Bold" } },
      "font_pairings": { "classic-editorial": { "name": "Classic Editorial", "title": "playfair-display", "subtitle": "lora", "body": "open-sans" } }
    }
  }
}

scheme is skin-custom while the custom palette is on. roles are the five colours the Colour Scheme page shows; palette is the thirteen colours the store uses. The example lists are shortened.

Update Design

PATCH /api/store/design

Ability: store:write. The same saves as Design > Colour Scheme and Design > Typography. Send any of these:

Field Type Description
scheme string One of choices.schemes. Turns the custom palette off.
custom_palette object Your own colours: any of accent, background, text, titles, buttons (hex), and overrides, an object of any palette key to a hex colour. Roles you leave out keep their current colour. Turns the custom palette on.
fonts object Any fonts key to one of its choices.fonts values.
font_pairing string One of choices.font_pairings. Sets the three font families; a font you also send in fonts wins.
{ "custom_palette": { "accent": "#E8692C", "background": "#FFFFFF", "text": "#1F2328", "titles": "#2B3036", "buttons": "#B4451A" }, "fonts": { "TITLE_FONT": "nunito", "TITLE_FONT_WEIGHT": "bold" } }

The answer is the Get Design body plus adjustments: colours the store changed so text stays readable (each pair is checked for contrast, overrides too).

  • scheme and custom_palette together, an unknown scheme, colour, font or value: 422 with an error per field. Nothing is written.
  • custom_palette needs the new design on the store, like the page: 409 with error: new_design_off otherwise.

Preview a Palette

POST /api/store/design/palette

Ability: store:write. A palette to look at. Nothing is saved.

Field Type Required Description
roles object No Any of accent, background, text, titles, buttons as hex. The store derives the full palette from them. Free.

Without roles, the AI designs five role colours for your niche and store name, the Colour Scheme page's "Generate with AI" button. Uses a few hundred AI chips. 402 when out of chips, 503 when the AI call fails.

{ "data": { "source": "ai", "roles": { "accent": "#1D6F42", "background": "#F7FAF7", "text": "#1E2B24", "titles": "#10241A", "buttons": "#1D6F42" }, "palette": { "COLOUR_ACCENT": "#1D6F42" }, "adjustments": {} } }

To apply it, send roles back as custom_palette to Update Design.


POST /api/store/featured-products

Ability: store:write. Chooses the products in the homepage Featured Products section: a varied set spread across your top categories, main items before accessories, priced products first, only products with a live, in-stock offer.

Field Type Required Description
quantity integer No 4 to 12. Default 8. The section never shrinks: a larger quantity you set stays.
force boolean No true chooses again over products already chosen. Default false.
dry_run boolean No true answers what it would choose and changes nothing.
{ "data": { "status": "seeded", "section_id": 25, "before": [], "after": [5, 116, 7, 107], "quantity": 4, "dry_run": false, "message": "Featured products chosen.", "products": [{ "id": 5, "title": "..." }] } }

Always 200. status says what happened:

status Meaning
seeded Chosen (or would be, with dry_run).
already_chosen Products are already chosen. Nothing changed. Send force: true to choose again.
blocked_draft The Store Editor holds unpublished changes. Publish or discard them first.
no_homepage The store has no homepage.
no_band The homepage has no Featured Products section switched on.
too_few_products Fewer than four products with a live offer. Nothing changed.

To set the products yourself, update the section's custom_ids with Update Page Sections.


POST /api/store/footer-credit

Ability: store:write. Sets the wording of the "Powered by FreshStore" footer credit on every template. You send plain text; the store builds the link. The link always points at the FreshStore homepage and becomes your referral link automatically if you are in the referral programme.

Field Type Required Description
before string No Text before the link, max 200 chars
link string Yes Link text, max 100 chars
after string No Text after the link, max 200 chars
show boolean No true turns the credit back on where it was hidden. Omit to keep each template's own choice.
{ "data": { "templates": 6, "shown": false } }

Recompute Store State

POST /api/recompute

Ability: both settings:write and store:read on a scoped key. No body. Optional query ?limit= (recommendation cards, 1 to 50).

Re-checks the launch plan against the store's real data, saves the result, and returns the launch plan and the recommendations in one response. Use it after changing settings when you want the launch plan to reflect the change straight away. It never returns 500 for a failed half: a half that could not be computed is null.

{
  "data": {
    "launch_plan": { "percent": 60, "launch_complete": false, "next_step": { "...": "..." }, "sections": [ ] },
    "recommendations": [ { "key": "...", "title": "...", "...": "..." } ]
  }
}

Tools

Get Offer Health

GET /api/tools/offer-health

Ability: store:read. Counts of what POST /api/store/clean would remove, using the same rules.

{
  "data": {
    "expired_offers": 3,
    "out_of_stock_offers": 7,
    "products_without_offers": 2,
    "empty_brands": 1,
    "empty_categories": 0,
    "checked_at": "2026-10-07T12:00:00+00:00"
  }
}

Redirects

Ability: settings:read to list, settings:write to create or delete. The AI key presets leave settings out, so they cannot change redirects.

Redirect Fields

Field Type Description
id integer
from string Old path, e.g. /products/old-thing
from_url string Old address as a full URL
to string Where it goes, as a label
to_url string Where it goes, as a full URL
status string active, waiting (old page still live), or broken (new address not working)
status_label string Human label for status
source string Who made it. Automatic redirects from slug changes are marked.
last_used_at datetime
created_at datetime
type, old_slug, new_slug, source_still_live Older fields, kept for older callers

List Redirects

GET /api/redirects

Newest first.

Parameter Type Default Description
per_page integer 50 1 to 100
hide_automatic boolean false Hide redirects the store made itself when a slug changed
{
  "data": [ { "id": 12, "from": "/products/old-thing", "to_url": "https://store.example.com/products/new-thing", "status": "active", "...": "..." } ],
  "meta": { "total": 1, "per_page": 50, "current_page": 1 }
}

Create Redirect

POST /api/redirects
Field Type Required Description
from string Yes* Old address. Any form: full URL, path or slug. Max 255 chars.
to string Yes* New address. Any form. Max 255 chars.
switch_off_source boolean No Also disable the old page, so the redirect takes effect
type, old_slug, new_slug string No Older request shape. type is page, product, category, article or brand. Required only when from / to are not sent.

A second redirect from the same address replaces the first. An address the store cannot use returns 422 with a message.

{ "data": { "id": 13, "from": "/old", "to_url": "https://store.example.com/new", "status": "active", "...": "..." } }

Create Redirects in Bulk

POST /api/redirects/bulk
Field Type Required Description
redirects array Yes 1 to 500 rows, each with the fields of Create Redirect (from, to, switch_off_source)

Each row is checked exactly like Create Redirect. A row that fails is reported with its position (index, from 0) and message, and the other rows are still saved. The rows are saved together, and the CDN is purged once for the lot. More than 500 rows returns 422 and saves nothing.

{
  "data": {
    "total": 2,
    "created": 1,
    "failed": 1,
    "results": [
      { "index": 0, "ok": true, "redirect": { "id": 14, "from": "/products/old-thing", "status": "active", "...": "..." } },
      { "index": 1, "ok": false, "error": "Your homepage always works, so it cannot be redirected. Enter the old address that has stopped working." }
    ]
  }
}

Delete Redirect

DELETE /api/redirects/{id}

Response 204 (no content).


Users

Ability: users:read / users:write.

The user endpoints also check who made the key. A key made by an Admin cannot see, change, delete or sign in as users above it (for example the store owner). The platform's own machine account is exempt. Breaking this rule returns 403 {"error": "Forbidden"}.

User Fields

Field Type Description
id integer
firstName string
lastName string
email string
roles string[] Role names
hidden boolean Hidden from the admin's user list
lastLoginAt datetime Null if they have never signed in

List Users

GET /api/users

Paginated. Only the users the key's owner may manage.

Create User

POST /api/users

If the email already exists in this store (including a deleted user), that user is restored and returned instead of an error.

Field Type Required Description
email string Yes Valid email, max 255 chars
password string No A random password is set if omitted
first_name string No Max 255 chars
last_name string No Max 255 chars
roles string[] No Existing role names. Super Admin, Admin and Owner need a Full Access (*) key, otherwise 403.
newsletter boolean No Default true
hidden boolean No Needs a Full Access (*) key, otherwise 403

Response 201 for a new user, 200 for an existing one, with the user in data.

Invite User (platform only)

POST /api/users/invite

Only the platform's machine account may call this; any other key gets 403. Invites someone as an Admin of this store and emails them a 7-day link to set their own password. Limit: 10 invited people per store.

Field Type Required Description
email string Yes
first_name string Yes
last_name string No
invited_by string No Name shown in the email, max 100 chars
{ "data": { "user": { "id": 7, "email": "helper@example.com", "...": "..." }, "created": true } }

Resend Invite (platform only)

POST /api/users/{id}/invite

Platform machine account only. Sends a fresh 7-day link to an invited Admin. Optional body {"invited_by": "Name"}. Returns the user in data. 422 for the owner, hidden accounts or anyone who is not an invited Admin.

GET /api/users/login-link?email=jane@example.com

Returns a magic link that signs the holder in as that user. Treat it like a password. Needs a Full Access key: any other key gets 403 (since 2026-10-07).

Parameter Type Required Description
email string Yes Must belong to a user in this store
admin boolean No Default true: the link opens the admin area
target string No Admin page to land on: products, categories, articles, pages, design, store-editor, homepage, media, settings, api-keys, launch-plan, amazon-settings, ebay-settings, colour-scheme, social-settings, keyword-research, product-finder, article-writer, tools, store-health, offer-settings. Anything else lands on the dashboard.
{ "data": { "loginLink": "https://store.example.com/magiclink/..." } }

Get User

GET /api/users/{id}

Update User

PATCH /api/users/{id}
Field Type Required Description
first_name string No Max 255 chars
last_name string No Max 255 chars
email string No Valid email, max 255 chars

Delete User

DELETE /api/users/{id}

Removes the user's access: their API keys and any pending invite are deleted, then the user. The store owner, hidden and FreshStore accounts, and the key's own user cannot be removed (422). Response 204.


Settings

Ability: settings:read / settings:write.

Get All Settings

GET /api/settings

Every setting as flat {group}_{setting} keys. Credential-shaped keys are removed: any key ending in _password, _secret, _key, _keystring, _key_base64, _token or _cert_id. Also includes design_new_design_enabled (read only).

{
  "data": {
    "store_name": "My Fresh Store",
    "store_niche": "tech",
    "product_show_reviews": true,
    "offers_amazon_enabled": true
  }
}

Update Settings (flat)

PATCH /api/settings

Send one or more {group}_{setting} keys.

{ "store_name": "My Updated Store", "product_show_reviews": false }
  • A key that names no settings group is ignored.
  • A key that names a real group but cannot be saved makes the request fail with 422 and lists the key: {"message": "Some settings could not be saved.", "failed": ["store_skin"]}. The other keys in the same request are still saved.
  • store_launch_checklist cannot be written here. Use the Launch Plan endpoints.
  • template_frontend cannot be changed with a member key. Sending the current value is accepted. Use Switch Store Template.
  • A saved xml-sitemap_* key queues a sitemap rebuild within about ten minutes.

Success: {"message": "Settings saved successfully."} (no data wrapper).

Get Settings by Group

GET /api/settings/{group}

{group} is one of store, offers, product, traffic, article, social, template, xml-sitemap, schema. Keys come back without the group prefix.

The store group hides its internal keys. Every group drops credential-shaped keys (since 2026-10-07), with the same rule as GET /api/settings: any key ending in _password, _secret, _key, _keystring, _key_base64, _token or _cert_id. So GET /api/settings/offers no longer returns merchant API keys and secrets, and GET /api/settings/traffic no longer returns the IndexNow key. You can still write them with PATCH.

{ "data": { "name": "My Fresh Store", "niche": "tech", "live": true, "default_country": "US" } }

Update Settings by Group

PATCH /api/settings/{group}

Send keys without the group prefix. Same rules and responses as the flat update; failed lists your own key names. In the social group, pasted full profile URLs are reduced to handles. Changing store.ai_language also purges the store's cached pages.

{ "name": "My Updated Store", "niche": "tech" }

Settings groups and fields

Field names are as returned by GET /api/settings/{group}. Prefix with {group}_ for the flat endpoints (store_name, xml-sitemap_products_quantity).

store

Field Type Notes
name string Store name
niche string
live boolean Store open to the public
ai_language string Language for AI text and the page lang
default_country string
enabled_countries string[]
cart_enabled boolean
cart_show_buy_now boolean
advanced_mode boolean
header_scripts string HTML added to every page head
footer_scripts string HTML added before the closing body tag
ads_txt_content string
enable_message_email_notifications boolean
matomo_site_id string
launch_checklist object Read only here; see Launch Plan

Hidden from this group (not returned, managed by the platform): url, url_private, version, user_id, website_id, the stock image provider keys (pixabay_api_key, pexels_api_key, unsplash_api_key), stream_*, api_cacher_*, aws_ses_smtp_*, openai_api_key, matomo_api_url, matomo_api_key, helpscout_beacon_key. The platform manages them; do not write them.

offers

Field pattern Type Notes
{merchant}_enabled boolean amazon, ebay, etsy, walmart, aliexpress
{merchant}_search_button_enabled boolean Same merchants
amazon_{cc}_tag string Your Amazon tracking ID per marketplace. cc: au be br ca de eg es fr gb ie in it jp mx nl pl sa se sg tr ae us za
amazon_{cc}_tag_default string Platform default tag per marketplace
amazon_{cc}_api_key, amazon_{cc}_api_secret, amazon_{cc}_oauth_client_id, amazon_{cc}_oauth_client_secret, amazon_{cc}_credential_version string Amazon API credentials per marketplace
amazon_tag_overridden_locales string[] Marketplaces whose tag was set in this store, so an account-level tag does not replace it
amazon_link_type string product_page or add_to_cart
amazon_direct_link_fallback boolean
aliexpress_tracking_id, aliexpress_api_key, aliexpress_api_secret string Also per country: aliexpress_{cc}_... for us gb it es de in
aliexpress_tracking_id_overridden boolean
ebay_tracking_id, ebay_app_id, ebay_cert_id string Also per country: ebay_{cc}_... for us gb at au be ca ch de es fr hk ie in it my nl ph pl sg th tw vn
ebay_tracking_id_overridden boolean
etsy_tracking_id, etsy_keystring, etsy_shared_secret string Also per country: etsy_{cc}_... for us ca au gb in de fr nl it es ru pt gr ie jp nz pl
walmart_affiliate_id, walmart_consumer_id, walmart_api_key_base64 string Also walmart_us_...
preferred_network string or null Merchant whose offers sort first on the product page
auto_refresh_offers boolean Refresh stale offers in the background
auto_refresh_offers_minutes integer Default 4320
show_estimated_price boolean
hide_estimated_price boolean Unused

product

Field Type Notes
auto_review_quantity_minimum, auto_review_quantity_maximum integer Range for automatic review counts
auto_review_score_minimum, auto_review_score_maximum float Range for automatic review scores
show_reviews boolean
show_ebay_related_products boolean
auto_generate_content_with_ai boolean
auto_generate_product_titles_with_ai boolean
show_button_on_listings boolean
listing_default_sort string default, newest, price-asc, price-desc, rating, title

traffic

Field Type Notes
indexnow_api_key string
indexnow_submit_pages, indexnow_submit_products, indexnow_submit_product_categories, indexnow_submit_brands, indexnow_submit_articles, indexnow_submit_article_categories boolean
robots_txt_content string
gsc_verification_content string Google Search Console verification
bing_verification_content string Bing Webmaster verification
ai_treasure_map_include_products, ..._product_categories, ..._articles, ..._article_categories, ..._pages boolean What llms.txt lists

article

Field Type Notes
default_author string
show_article_meta boolean
show_table_of_contents boolean
auto_refresh_modified_date_enabled boolean
auto_refresh_modified_date_after_days integer Default 90
auto_refresh_modified_date_spread_days integer Default 14

social

All strings: twitter_handle, facebook_page_handle, facebook_group_handle, youtube_channel_handle, instagram_handle, tiktok_handle, pinterest_handle, linkedin_page_handle, tumblr_handle, discord_invite, whatsapp_url, custom_social_url, custom_social_label.

template

Field Type Notes
frontend string The active template key. Read only for member keys: sending a different value is refused. Switch with Switch Store Template, which carries your content across.

xml-sitemap

Field Type Notes
products_quantity, categories_quantity, pages_quantity, articles_quantity, brands_quantity integer Cap per type. 0 means include everything.
product_method string recent, popular, oldest, random

schema

What the store sends to Google as structured data. All true by default. None of them changes the stars visitors see on the store.

Field Type Notes
price_and_availability boolean The product price and whether it is in stock
shipping_and_returns boolean The standard shipping and returns details on each product offer
article_author boolean The author name on articles
generated_stars_in_google boolean The star rating on products that have no rating of your own
your_ratings_in_google boolean Your own ratings (author_rating) on products and review articles

Launch Plan

Ability: settings:read / settings:write.

Get Launch Plan

GET /api/launch-plan

Re-checks the plan against the store's data, then returns it.

{
  "data": {
    "percent": 60,
    "launch_complete": false,
    "next_step": { "key": "store_logo_set", "title": "...", "section": "...", "...": "..." },
    "sections": [
      {
        "title": "Set up your store",
        "description": "...",
        "counted": true,
        "ongoing": false,
        "steps": [
          {
            "key": "store_niche_set",
            "title": "...",
            "description": "...",
            "done": true,
            "manual": false,
            "target": "settings",
            "button_label": "Open",
            "repeats": null
          }
        ]
      }
    ]
  }
}

manual: true means the store cannot detect the step itself.

Mark Step Done

POST /api/launch-plan/{key}

No body. {key} is a step key from GET /api/launch-plan. Unknown key: 404.

{ "data": { "key": "bing_webmaster_set", "done": true, "percent": 65 } }

Mark Step Not Done

DELETE /api/launch-plan/{key}
{ "data": { "key": "bing_webmaster_set", "done": false, "percent": 60 } }

Recommendations

Get Recommendations

GET /api/recommendations

Ability: store:read. What the store suggests doing next, built live on every call. Read only.

Parameter Type Default Description
limit integer 10 Cards to return, 1 to 50
{
  "data": [
    {
      "key": "thin_category",
      "subject_type": "category",
      "subject_id": 12,
      "title": "...",
      "body": "...",
      "icon": "...",
      "color": "...",
      "severity": "...",
      "priority": 50,
      "band": "...",
      "dismissible": true,
      "snooze_hours": 168,
      "cta": { "type": "...", "label": "...", "url": "...", "external": false }
    }
  ]
}

Member Facts (platform)

Push Member Facts

POST /api/member-facts

Platform only: the store's own system account. A member key gets 403, whatever its abilities. Used by the FreshStore platform to tell a store facts about its owner's account. It replaces the stored row: a field you leave out becomes null.

Field Type Required Description
plan_tier string No Max 64 chars
store_count integer No 0 to 100000
account_last_login_at date No
estimated_earnings_rate number No 0 to 1
estimated_conversion_rate number No 0 to 1
estimated_commission_rate number No 0 to 1
video_standing object No allowance_bytes, used_bytes, over (required with the object), optional limits.videos, limits.stored_minutes, limits.file_mb

Returns the stored facts in data, plus synced_at.


Products

Product Fields

Field Type Description
id integer
slug string
title string
subtitle string
link_title string
content string HTML content
content_bottom string HTML content below main
highlights string[] Key features / bullet points
summary string
meta_title string
meta_keywords string
meta_description string
review_quantity integer
review_score float
author_rating float Your own rating, 1 to 5 in steps of 0.5. Shown as "Our rating" in place of the stars above
author_verdict string Your verdict, max 600 chars
author_pros string[] What you like, max 8 items of 160 chars
author_cons string[] What is worth knowing, max 8 items of 160 chars
author_reviewed_at datetime When the rating last changed (read only, set by the store)
enabled boolean
in_sitemap boolean
brand_id integer
analytics_visits integer Total page views
upc string
ean string
isbn string
mpn string
main_image_url string Primary product image URL (read only)
created_at datetime
updated_at datetime
brand object With ?include=brand
offers array With ?include=offers
categories array With ?include=categories

Categories are not a product field you can write. See Set Product Categories.

List Products

GET /api/products
Parameter Type Description
search string Search by title
enabled boolean Filter by enabled status
brand_id integer Filter by brand
category_id integer Filter by category
sort_by string One of id, title, slug, enabled, brand_id, analytics_visits, review_score, review_quantity, author_rating, created_at, updated_at (default created_at). Anything else returns 422. Products with no author_rating sort last when descending.
sort_dir string asc or desc (default desc)
own_touch string with or without: products with or without something of your own (a verdict, pros or cons text, or a photo you uploaded). Anything else returns 422.

Create Product

POST /api/products
Field Type Required Description
title string Yes Max 255 chars
slug string No Max 255 chars
subtitle string No Max 255 chars
link_title string No Max 255 chars
content string No HTML
content_bottom string No HTML
highlights string[] No
summary string No
meta_title string No Max 255 chars
meta_keywords string No Max 255 chars
meta_description string No
review_quantity integer No
review_score number No
author_rating number No 1 to 5 in steps of 0.5 (4.5, not 4.3). null clears it
author_verdict string No Max 600 chars
author_pros string[] No Max 8 items, 160 chars each
author_cons string[] No Max 8 items, 160 chars each
enabled boolean No
in_sitemap boolean No
brand_id integer No Must exist in brands
upc string No Max 255 chars
ean string No Max 255 chars
isbn string No Max 255 chars
mpn string No Max 255 chars

Response 201 with the product in data.

Rating fields on a store mid-upgrade. A store that has not finished its latest upgrade answers 409 with "Store is upgrading, retry in a minute." when the request carries an author_* field. Retry after a minute. To rate many products, send one PATCH per product.

A product made this way has no offer, so no price and no image. Add one with POST /api/offers and POST /api/offers/{id}/products, or import Amazon products with POST /api/products/import-links. Then set its categories with POST /api/products/{id}/categories.

POST /api/products/import-links

Ability: products:write. Imports Amazon products from links, like the admin's Link Importer. Imports run in the background.

Field Type Required Description
links string[] Yes 1 to 100 Amazon links, ASINs or pasted text
categories integer[] No Category ids to put the new products in
locale string No Marketplace for bare ASINs. Default us. One of au be br ca cn de eg es fr gb ie in it jp mx nl pl sa se sg tr ae us za.

Response 202

{
  "data": {
    "total": 3,
    "importing": 1,
    "duplicates": 1,
    "failed": 1,
    "links": [
      { "link": "https://www.amazon.com/dp/B0EXAMPLE1", "asin": "B0EXAMPLE1", "locale": "us", "status": "importing" },
      { "link": "...", "asin": "B0EXAMPLE2", "locale": "us", "status": "duplicate", "reason": "Product already exists", "product_id": 41 },
      { "link": "not a link", "status": "failed", "reason": "...", "reason_code": "..." }
    ]
  }
}

Get Product

GET /api/products/{id}

Use ?include=brand,offers,categories to load related data.

{
  "data": {
    "id": 1,
    "title": "Example Product",
    "brand": { "id": 5, "title": "Acme" },
    "offers": [{ "id": 10, "title": "..." }],
    "categories": [{ "id": 3, "title": "..." }]
  }
}

Update Product

PATCH /api/products/{id}

Same fields as Create, all optional. Only the fields you send change. Returns the product in data.

PATCH does not set categories. Sending categories or category_ids returns 422:

{
  "message": "Set product categories with POST /api/products/{id}/categories and {\"category_ids\": [...]}.",
  "errors": { "category_ids": ["Set product categories with POST /api/products/{id}/categories and {\"category_ids\": [...]}."] }
}

Stores on a release from before 2026-10-07 drop those fields without an error and still answer 200, so do not rely on the 422. Always use Set Product Categories.

Delete Product

DELETE /api/products/{id}

Soft-deletes the product. Response 204 (no content).

Get Product Offers

GET /api/products/{id}/offers

Paginated list of offers linked to this product.

Get Product Categories

GET /api/products/{id}/categories
{ "data": [{ "id": 1, "title": "Electronics", "...": "..." }] }

Set Product Categories

POST /api/products/{id}/categories

Ability: products:write. Replaces the product's categories with exactly the list you send. Send [] to remove the product from every category.

Field Type Required Description
category_ids integer[] Yes Every id must exist in categories
{ "category_ids": [1, 2, 3] }

Response:

{ "data": { "category_ids": [1, 2, 3] } }

Get Product Analytics

GET /api/products/{id}/analytics
{ "data": { "analytics_visits": 1250, "total_clicks": 89, "affiliate_clicks": 45 } }

Set Product Image

POST /api/products/{id}/images

Ability: products:write. Adds a picture to the product's own gallery from an image URL, as the Images tab on the product form does. The product shows its own pictures before any offer picture.

Field Type Required Description
collection string Yes gallery
url string Yes A public http or https address of a JPEG, PNG, WebP or GIF, at most 10 MB. Redirects are not followed, so send the final address.
replace boolean No Default false: the picture goes last in the gallery. true removes the gallery's pictures first, which makes this one the main picture. Offer pictures are not touched.
{ "data": { "collection": "gallery", "url": "https://.../copper-kettle.jpg", "replaced": false, "added": true, "count": 2 } }

count is the number of pictures in the gallery now. A URL already in the gallery is not stored twice: added is then false. A URL the store cannot use returns 422 with the reason in message.


Offers

Offer Fields

Field Type Description
id integer
uuid string For merchant offers: {merchant}_{locale}_{merchant_offer_id}
merchant string amazon, ebay, etsy, walmart, aliexpress or custom
merchant_offer_id string The merchant's item id (ASIN for Amazon)
merchant_product_id string Older name for merchant_offer_id, same value
title string
summary string
features array
currency string e.g. USD, GBP
price float
price_full float Original / list price
price_hidden boolean The store does not show this price to visitors. Anything that publishes prices must respect it.
price_source string Where the price came from
price_checked_at datetime When the price was last checked
min_price float
max_price float
condition string e.g. new, used
stock_available boolean
enabled boolean
locale string e.g. us, gb
images array
url_detail string
created_at datetime
updated_at datetime

Get Enabled Merchants

GET /api/offers/merchants
{ "data": { "amazon": "Amazon", "ebay": "eBay", "custom": "Custom" } }

List Offers

GET /api/offers

Newest first.

Parameter Type Description
search string Search by title
merchant string Filter by merchant
enabled boolean Filter by enabled status
product_id integer Filter by product

Create Offer

POST /api/offers
Field Type Required Description
merchant string Yes amazon, ebay, etsy, walmart, aliexpress or custom (case-insensitive). Anything else is 422. Use custom for your own stock or any other shop.
title string Yes Max 255 chars
url_detail string Yes Offer URL
merchant_offer_id string Conditional Required unless merchant_product_id is sent. A bare eBay item number is accepted.
merchant_product_id string No Older name for merchant_offer_id
uuid string No Custom offers only; ignored for merchant offers
summary string No
features array No
currency string No Max 10 chars
price number No
price_full number No
min_price number No
max_price number No
condition string No Max 50 chars
stock_available boolean No
enabled boolean No Default true
locale string No Max 10 chars, default us
images array No

For a merchant offer (not custom) the store keeps one offer per merchant, locale and item. Creating one that already exists returns the existing offer (restored and enabled if needed). The store then fetches price, stock and images from the merchant in the background. Link the offer to a product with POST /api/offers/{id}/products.

Response 201 with the offer in data.

Get Offer

GET /api/offers/{id}

Update Offer

PATCH /api/offers/{id}

Same fields as Create except uuid, all optional.

Delete Offer

DELETE /api/offers/{id}

Removes the offer from every product that uses it. Response 204 (no content).

POST /api/offers/{id}/products

Ability: offers:write. Adds the offer to the products you list. Products already linked stay linked; nothing is removed. To take an offer off everything, delete the offer.

Field Type Required Description
product_ids integer[] Yes Every id must exist in products
{ "product_ids": [1, 2, 3] }

Response: every product the offer is now linked to.

{ "data": { "product_ids": [1, 2, 3, 9] } }

Categories

Category Fields

Field Type Description
id integer
parent_id integer Parent category (null = top level)
slug string
title string
subtitle string
link_title string
content_top string HTML shown above products
content_bottom string HTML shown below products
meta_title string
meta_keywords string
meta_description string
enabled boolean
sort_order integer
products_count integer On list, get and tree
children_count integer On list, get and tree
children array On the tree only
created_at datetime
updated_at datetime

Get Category Tree

GET /api/categories/tree

All categories nested under their parents, ordered by sort_order.

{
  "data": [
    { "id": 1, "title": "Electronics", "children": [ { "id": 5, "title": "Phones", "children": [] } ] }
  ]
}

List Categories

GET /api/categories

Ordered by sort_order.

Parameter Type Description
search string Search by title
enabled boolean Filter by enabled status
parent_id integer Filter by parent

Create Category

POST /api/categories
Field Type Required Description
title string Yes Max 255 chars
slug string No Max 255 chars
parent_id integer No Must exist in categories
subtitle string No Max 255 chars
link_title string No Max 255 chars
content_top string No
content_bottom string No
meta_title string No Max 255 chars
meta_keywords string No Max 255 chars
meta_description string No
enabled boolean No
sort_order integer No
generate_ai_content boolean No Write the subtitle, link title, top and bottom content and meta with AI in the background. Uses AI chips.
generate_images boolean No Make the category banner and listing image in the background. See image_source.
image_source string No auto (default), stock, or ai. See Image source.
image_quality string No standard (default) or high. AI images only. See Image quality.

Response 201 with the category in data. To put products in it, use POST /api/products/{id}/categories, POST /api/products/import-links with categories, or POST /api/categories/fill.

Generate Category List (AI)

POST /api/categories/generate-list

Ability: categories:write. Suggests a category tree for a niche. Creates nothing; call POST /api/categories for each one you want. One AI call.

Field Type Required Description
niche string Yes Max 255 chars
max_categories integer No 1 to 20, default 5
max_subcategories integer No 0 to 10. Accepted but not used: the AI returns 3 to 5 subcategories each.
{
  "data": {
    "niche": "vintage cameras",
    "categories": [
      { "title": "Film SLRs", "subcategories": ["35mm", "Medium Format", "Manual Focus"] }
    ]
  }
}

402 when out of chips, 503 when the AI call fails.

Fill Categories with Products

POST /api/categories/fill

Ability: categories:write. Finds products from a merchant for each category and adds them, in the background. A one-off run, not an Auto Populator.

Field Type Required Description
api_name string Yes amazon, ebay, etsy, walmart or aliexpress
locale string Yes A locale that merchant supports, e.g. us. Otherwise 422.
categories integer[] No Up to 50 category ids. Default: every category.
quantity integer No Products per category, 1 to 50, default 5
generate_content boolean No Write product content with AI. Default true.
ai_relevance_check boolean No Let AI reject products that do not fit the category. Default true.

Response 202

{ "data": { "categories_queued": 8, "quantity_per_category": 5, "api_name": "amazon", "locale": "us" } }

Get Category

GET /api/categories/{id}

Update Category

PATCH /api/categories/{id}

Same fields as Create, all optional. generate_ai_content and generate_images regenerate on update too.

Delete Category

DELETE /api/categories/{id}

Response 204 (no content).

Get Category Products

GET /api/categories/{id}/products

Paginated list of products in this category.

Set Category Image

POST /api/categories/{id}/images

Ability: categories:write. Sets a category picture from an image URL.

Field Type Required Description
collection string Yes listing: the picture on category tiles, the All Categories page and the homepage, the category form's Listing Image. banner: the wide category picture that generate_images makes beside it.
url string Yes A public http or https address of a JPEG, PNG, WebP or GIF, at most 10 MB. Redirects are not followed, so send the final address.
replace boolean No Default true. false fills an empty slot and leaves a full one as it is.
{ "data": { "collection": "listing", "url": "https://.../copper-kettles.jpg", "replaced": true, "added": true } }

A URL the store cannot use returns 422 with the reason in message.


Auto Populators

An Auto Populator adds products to one category from one merchant search, a few a day. Ability: categories:read to read, categories:write to change. Create, delete and run-now stay in the admin.

Populator Fields

Field Type Description
id integer
category_id integer The category it fills
category_title string
enabled boolean Writable
search_term string Writable, max 255 chars
merchant string
search_options object Locale, merchant category, sort and price filters
quantity_per_day integer Writable: 1, 3, 5, 10, 25 or 50
max_quantity integer Writable. Maximum Products in total; 0 is unlimited
max_products_in_category integer Writable. Adds nothing while the category holds this many; 0 is no limit
ai_relevance_check boolean Writable
total_products_added integer
reached_max_products boolean true when total_products_added has reached max_quantity
stopped_reason string Why it is off, when it switched itself off
enabled_at, last_run_at, last_completed_at, created_at, updated_at datetime

List Populators

GET /api/populators

Paginated. Filters: enabled (boolean), category_id, search (on the search term).

Get Populator

GET /api/populators/{id}

Update Populator

PATCH /api/populators/{id}

Any of the writable fields. Switching on a populator that has reached its Maximum Products returns 422 with the reason, because it would switch itself off again on its next run. Raise max_quantity (or set it to 0) in the same call.

{ "enabled": true, "max_quantity": 200 }

Switch Many On or Off

POST /api/populators/bulk
Field Type Required Description
ids integer[] Yes 1 to 500 populator ids
enabled boolean Yes
{ "data": { "updated": [4, 9], "unchanged": [12], "reached_max_products": [15], "not_found": [] } }

Populators in reached_max_products stay off; raise their max_quantity with PATCH first.


Brands

Brand Fields

Field Type Description
id integer
slug string
title string
subtitle string
link_title string
content string
content_bottom string
meta_title string
meta_keywords string
meta_description string
enabled boolean
products_count integer On list and get
created_at datetime
updated_at datetime

List Brands

GET /api/brands

Ordered by title.

Parameter Type Description
search string Search by title
enabled boolean Filter by enabled status

Create Brand

POST /api/brands
Field Type Required Description
title string Yes Max 255 chars
slug string No Max 255 chars
subtitle string No Max 255 chars
link_title string No Max 255 chars
content string No
content_bottom string No
meta_title string No Max 255 chars
meta_keywords string No Max 255 chars
meta_description string No
enabled boolean No
generate_ai_content boolean No Write the subtitle, link title, content and meta with AI in the background. On update this overwrites that copy.

Response 201 with the brand in data. Link products to a brand with brand_id on the product.

Clean Brands (AI)

POST /api/brands/clean

Ability: brands:write. Tidies brand titles, then AI sorts up to 60 brands (most products first) into enable, keep disabled, or delete. Enabled brands get AI content. Deleted brands are removed from their products first. Uses AI chips.

Field Type Required Description
niche string No Helps the AI judge fit, max 255 chars
keep_top integer No 1 to 50. Enable at most this many; the rest stay disabled.
{ "data": { "classified": 24, "enabled": 10, "capped": 2, "deleted": 5, "kept_disabled": 9, "title_fixes": 3 } }

Get Brand

GET /api/brands/{id}

Update Brand

PATCH /api/brands/{id}

Same fields as Create, all optional.

Delete Brand

DELETE /api/brands/{id}

Response 204 (no content).

Get Brand Products

GET /api/brands/{id}/products

Paginated list of products for this brand.


Articles

Article Fields

Field Type Description
id integer
slug string
title string
subtitle string
link_title string
content string HTML
meta_title string
meta_keywords string
meta_description string
enabled boolean
generated integer 1 if AI-generated
product_category_id integer Product category the article is tied to
fallback_random_products boolean
formula string Layout: standard, review or fact-sheet. Empty uses the store default (read only here)
reviewed_product_id integer The product a review article rates
author_rating float Your rating of that product, 1 to 5 in steps of 0.5
author_verdict string
author_pros string[]
author_cons string[]
author_reviewed_at datetime When the rating last changed (read only)
published_at datetime
created_at datetime
updated_at datetime
article_categories array On get only

List Articles

GET /api/articles

Newest first.

Parameter Type Description
search string Search by title
enabled boolean Filter by enabled status
generated boolean Filter by AI-generated status
article_category_id integer Only articles in this article category

Create Article

POST /api/articles
Field Type Required Description
title string Yes Max 255 chars
slug string No Max 255 chars
subtitle string No Max 255 chars
link_title string No Max 255 chars
content string No HTML
meta_title string No Max 255 chars
meta_keywords string No Max 255 chars
meta_description string No
enabled boolean No
published_at date No ISO 8601. A future date schedules the article: it is saved disabled, AI generation still runs at once, and it goes live on that date with its date kept
product_category_id integer No A product category, for the sidebar and related products
article_categories integer[] No Article category ids
generate_ai_content boolean No Write the body, subtitle and meta with AI in the background. Uses AI chips.
generate_seo_keyword string No Seed keyword for generate_ai_content, max 255 chars
generate_images boolean or string[] No true for all three (listing, hero, social) or a subset, e.g. ["hero","social"]
image_source string No auto (default), stock, or ai. See Image source.
image_quality string No standard (default) or high. AI images only. See Image quality.
reviewed_product_id integer No The product this article reviews. Must exist
author_rating number No 1 to 5 in steps of 0.5. null clears it
author_verdict string No Max 600 chars
author_pros string[] No Max 8 items, 160 chars each
author_cons string[] No Max 8 items, 160 chars each

Response 201 with the article in data. A rating field on a store mid-upgrade answers 409, as for products.

Generate Article Topics (AI)

POST /api/articles/generate-topics

Ability: articles:write. Suggests article topics for the store. Creates nothing; pass a topic to POST /api/articles with generate_ai_content: true to write it. One AI call for the whole batch.

Field Type Required Description
count integer No 1 to 10, default 3
niche string No Max 255 chars. Default: the store's niche.
{
  "data": {
    "topics": ["Beginners Guide to Buying Your First Film Camera", "Caring for Vintage Camera Leather and Bellows"],
    "requested": 3,
    "returned": 2
  }
}

returned can be lower than requested when the AI returns duplicates or blanks.

Get Article

GET /api/articles/{id}

Includes article_categories.

Update Article

PATCH /api/articles/{id}

Same fields as Create, all optional. article_categories replaces the attached set. generate_ai_content: true clears the body and writes it again. generate_images without generate_ai_content remakes the images only.

Delete Article

DELETE /api/articles/{id}

Response 204 (no content).

Fill Empty Images

POST /api/articles/{id}/fill-empty-images

Ability: articles:write. Copies the article's hero image into the listing and social image slots when they are empty. Never overwrites. Does nothing if there is no hero image.

{ "data": { "filled": ["listingImage", "socialImage"] } }

Set Article Image

POST /api/articles/{id}/images

Ability: articles:write. Sets the article's hero, listing or social image from an image URL, replacing the one there. The store downloads the image itself.

Field Type Required Description
collection string Yes hero, listing or social
url string Yes A public http or https address of a JPEG, PNG, WebP or GIF, at most 10 MB. Redirects are not followed, so send the final address.
{ "data": { "collection": "hero", "url": "https://.../my-article-hero.jpg" } }

A URL the store cannot use returns 422 with the reason in message.

Search Stock Images

GET /api/images/stock

Ability: articles:read. The editor's Find Stock Image search over the free stock photo libraries the store uses (Pexels, Unsplash, Pixabay). No AI chips. Pass a result's url to any image endpoint: Set Article Image, Set Product Image, Set Category Image or Set Store Image.

Parameter Type Required Description
query string Yes 2 to 100 chars
orientation string No horizontal (default), vertical or all
quantity integer No 1 to 12 (default 6)
{ "data": [ { "url": "https://images.pexels.com/...", "thumbnail": "https://...", "width": 4000, "height": 2667, "provider": "pexels", "description": "A shark underwater", "attribution": { "photographer": "...", "photographer_url": "...", "source_url": "..." } } ] }

Article Categories

Article Category Fields

Field Type Description
id integer
parent_id integer Parent category
slug string
title string
subtitle string
link_title string
content_top string
content_bottom string
meta_title string
meta_keywords string
meta_description string
enabled boolean
sort_order integer
articles_count integer On list and get
created_at datetime
updated_at datetime

List Article Categories

GET /api/article-categories

Ordered by sort_order.

Parameter Type Description
search string Search by title
enabled boolean Filter by enabled status

Create Article Category

POST /api/article-categories
Field Type Required Description
title string Yes Max 255 chars
slug string No Max 255 chars
parent_id integer No Must exist in article categories
subtitle string No Max 255 chars
link_title string No Max 255 chars
content_top string No
content_bottom string No
meta_title string No Max 255 chars
meta_keywords string No Max 255 chars
meta_description string No
enabled boolean No
sort_order integer No

Response 201

Get Article Category

GET /api/article-categories/{id}

Update Article Category

PATCH /api/article-categories/{id}

Same fields as Create, all optional.

Delete Article Category

DELETE /api/article-categories/{id}

Response 204 (no content).


Pages

Page Fields

Field Type Description
id integer
key string Stable id for system pages (e.g. homepage, about, contact). Null for your own pages.
slug string
title string
subtitle string
link_title string
content string HTML
meta_title string
meta_keywords string
meta_description string
enabled boolean
created_at datetime
updated_at datetime

List Pages

GET /api/pages

Newest first.

Parameter Type Description
search string Search by title
key string Exact match on key, e.g. ?key=homepage
enabled boolean Filter by enabled status

Create Page

POST /api/pages
Field Type Required Description
title string Yes Max 255 chars
key string No System page id, max 255 chars. Decides which template image slots generate_images fills on update. Set with care.
slug string No Max 255 chars
subtitle string No Max 255 chars
link_title string No Max 255 chars
content string No
meta_title string No Max 255 chars
meta_keywords string No Max 255 chars
meta_description string No
enabled boolean No
generate_ai_content boolean No Write the title, subtitle, meta and (for the homepage) the hero text with AI in the background. Uses AI chips.

Response 201

Get Page

GET /api/pages/{id}

Update Page

PATCH /api/pages/{id}

Same fields as Create, all optional, plus:

Field Type Required Description
generate_images boolean No Make the page's template images in the background (homepage hero, offer box and social image, or the about page image), chosen by key
image_source string No auto (default), stock, or ai. See Image source.
image_quality string No standard (default) or high. AI images only. See Image quality.

generate_ai_content: true works here too, for example on the homepage page of a new store.

Delete Page

DELETE /api/pages/{id}

Response 204 (no content).

Get Page Sections

GET /api/pages/{id}/sections

Ability: pages:read. The page's Sections for the active template, in order, enabled or not, and the blocks you can add, each with its settings. The same composition Design > Editor shows. Sections belong to the homepage, your own pages and any page already built from sections.

{
  "data": {
    "target": { "type": "page", "id": 1, "key": "homepage", "title": "Home" },
    "template": "crystal",
    "editable": true,
    "editor_draft": false,
    "sections": [
      { "id": 25, "block": "products", "name": "Products", "position": 10, "enabled": true, "placeable": true, "settings": { "title": "Vans at our pick-up cities", "mode": "custom", "custom_ids": [5, 116], "quantity": 8 } },
      { "id": 39, "block": "faq", "name": "FAQ", "position": 20, "enabled": true, "placeable": true, "settings": { "title": "Questions", "items": [{ "question": "...", "answer": "<p>...</p>" }] } }
    ],
    "blocks": [
      { "key": "faq", "name": "FAQ", "description": "...", "settings": [
        { "key": "title", "type": "text", "label": "Title", "default": "" },
        { "key": "items", "type": "repeater", "label": "Questions", "count": 6, "default": [], "fields": [{ "key": "question", "type": "text", "label": "Question", "default": "" }, { "key": "answer", "type": "wysiwyg", "label": "Answer", "default": "" }] }
      ] }
    ]
  }
}
  • editable is false while the store has the new design off, or for a stock page that keeps its own fixed layout.
  • editor_draft is true while Design > Editor holds unpublished changes for this page. Writes are refused until they are published or discarded.
  • placeable is false for a Section this page cannot draw. It shows nothing; remove it.
  • blocks[].settings is each block's settings schema. type is one of text, textarea, wysiwyg, code, number, boolean, select (with options), colour, date, link, image, product, products, select_multiple, repeater (with count and fields) and video. visible_when says which other setting a field depends on.

Update Page Sections

PATCH /api/pages/{id}/sections

Ability: pages:write. Applies a list of operations in order, all or nothing, and publishes the result at once (no draft). The answer is the Get Page Sections body plus result, the counts of Sections updated, created and deleted.

Field Type Required Description
operations array Yes 1 to 50 operations, below.
op Fields What it does
update id, settings, optional enabled Changes the settings you send; the others keep their value.
enable id Shows the Section.
disable id Hides the Section and keeps its settings.
move id, position Moves it to that place in the list, 1 being the top.
add block, optional settings, position, enabled Adds a Section from blocks, starting from its defaults. Without position it goes last; a hero goes first.
remove id Deletes the Section.
{
  "operations": [
    { "op": "update", "id": 39, "settings": { "title": "Questions people ask", "items": [{ "question": "Do I need a special licence?", "answer": "<p>Not for most campervans.</p>" }] } },
    { "op": "add", "block": "newsletter-signup", "position": 2 },
    { "op": "move", "id": 25, "position": 1 },
    { "op": "remove", "id": 31 }
  ]
}

How settings are written:

  • Each value is checked against the block's settings. An unknown setting, a select value not in its options, a bad colour or date, or a non-URL image src refuses the request.
  • A link is {"link": "https://...", "link_text": "", "new_tab": false, "no_follow": false}. link also takes an internal target such as internal:page:categories or internal:article:{slug}. Sub-keys you leave out keep their value.
  • An image is {"src": "https://...", "alt": "", "title": ""}. A new src drops the old picture's sized copies.
  • A repeater (FAQ questions, testimonials, cards) takes the whole list: send every item you want to keep, up to its count. Each item you send is merged onto the item at the same place, so a field you leave out keeps its value. Items past the end of your list are emptied.
  • products and select_multiple take a list of ids. Videos are set in Design > Editor; a video setting is refused.

Positions are renumbered to 10, 20, 30 and on whenever the order changes.

Status When
422 An operation is invalid: an unknown id, a block this page does not take, a refused setting. The errors keys name the operation, e.g. operations.1.settings.alignment. Nothing is written.
409 error: editor_draft (unpublished Editor changes for this page), new_design_off (the store does not show sections yet) or not_composable (a stock page with its own fixed layout).

Zones

Zones are the Sections above, below or beside the content of every category page, brand page or article, as arranged in Design > Editor. A zone belongs to the page type, not to one category, so there is no /api/categories/{id}/sections. Scoped keys need pages.

page_type Zones
category above_core, below_core, sidebar
brand above_core, below_core, sidebar
article above_core, below_core

The sidebar takes only blocks built for a narrow column; the others take what a page takes, except the hero. An unknown page type or zone returns 404.

Get Zone Sections

GET /api/zones/{page_type}/{zone}/sections

Ability: pages:read. Same body as Get Page Sections, with "target": {"type": "zone", "page_type": "category", "zone": "sidebar", "label": "Category pages: Sidebar"}.

Update Zone Sections

PATCH /api/zones/{page_type}/{zone}/sections

Ability: pages:write. Same operations and answers as Update Page Sections.


Image quality

Every endpoint that accepts generate_images (and POST /api/store/generate-logo) also accepts image_quality. It applies to images made with AI; stock images are unaffected.

Value Chips per AI image Time Notes
standard 20,000 ~15 seconds Default. Suits most images.
high 80,000 ~25 seconds Finer detail and more natural light.

Any other value returns 422. Leaving it out is the same as standard.


Image source (stock vs AI)

Endpoints that make images (generate_images on categories, articles and pages) accept image_source:

Mode Behaviour Chip cost
auto Default. Try Pexels, then Unsplash, then Pixabay. Use AI only if all three miss for that image. A few hundred chips per stock image (for the search keyword); about 20,000 per AI fallback (80,000 at high).
stock Stock only. Skip the image if nothing matches. A few hundred chips per attempt.
ai AI for every image. About 20,000 chips per image (80,000 at high).

How auto works for each image (banner, listing, hero, social):

  1. A short AI call builds a 2 to 5 word search term from the title, the store's niche and the image shape.
  2. Pexels, then Unsplash, then Pixabay are searched, stopping at the first match. A provider without an API key is skipped.
  3. A match is downloaded and attached like any other image.
  4. If nothing matches, an AI image is made.

Stock provider keys are set per store in the store settings group (pixabay_api_key, pexels_api_key, unsplash_api_key). With no keys set, auto behaves like ai.

Stock images record the provider and photographer in the media record's custom_properties.attribution.

You cannot choose a specific stock image through the API.


Analytics

Ability: analytics:read. Every analytics call queues a refresh from the analytics service when the store's copy is more than 10 minutes old, so the newest visits can lag by one refresh.

Summary parameters

The /summary and /by-merchant endpoints take:

Parameter Type Default Description
period string day day, week, or month
length integer 30 Number of periods to return. At most 366 days, 260 weeks or 120 months; more returns 422

Period keys look like 2026-10-07 (day), 2026-W41 (week) or 2026-10 (month). Periods with no activity are included as zeros.

Get Visits Summary

GET /api/analytics/visits/summary
{
  "data": {
    "2026-10-07": { "nb_uniq_visitors": 45, "nb_visits": 52, "nb_actions": 120 }
  }
}

List Visits

GET /api/analytics/visits

Newest first.

Parameter Type Description
country_code string Filter by country (e.g. US)
device_type string Filter by device (e.g. Desktop)
from date Start date
to date End date

Fields: id, ip_address, country_code, region, city, visits, actions, events, visit_duration_seconds, referrer_type_name, device_type, browser, first_action_at, last_action_at.

List Visitors

GET /api/analytics/visitors

Most recently seen first. Fields: id, matomo_visitor_id, first_seen, last_seen, visits_count, clicks_count.

Get Visitor

GET /api/analytics/visitors/{id}

Get Pages Analytics

GET /api/analytics/pages

Not paginated.

{
  "data": [
    { "id": 1, "label": "/products/example", "visitors": 120, "hits": 145, "average_time_on_page": 42, "bounce_rate": "35%", "exit_rate": "20%" }
  ]
}

Get Countries Analytics

GET /api/analytics/countries

Not paginated.

{
  "data": [
    { "id": 1, "locale": "us", "country_name": "United States", "visitors": 500, "visits": 620, "actions": 1800, "visit_duration_seconds": 45000 }
  ]
}

Get Devices Analytics

GET /api/analytics/devices

Not paginated.

{
  "data": [
    { "id": 1, "device": "Desktop", "visitors": 300, "visits": 400, "actions": 1200, "visit_duration_seconds": 30000 }
  ]
}

Get Sources Analytics

GET /api/analytics/sources

Not paginated.

{
  "data": [
    { "id": 1, "source": "Google", "visitors": 200, "visits": 250, "actions": 600, "visit_duration_seconds": 18000 }
  ]
}

Sources Summary

GET /api/analytics/sources/summary

Unique visitors over time, optionally for one traffic source. Takes the summary parameters plus:

Parameter Type Description
source string Part of a source label, e.g. google or Search Engines. Case-insensitive.
{
  "data": {
    "total": { "visitors": 340 },
    "periods": { "2026-10-06": { "visitors": 12 }, "2026-10-07": { "visitors": 9 } }
  }
}

Affiliate Clicks Summary

GET /api/analytics/clicks/summary

All clicks and affiliate clicks over time. Takes the summary parameters.

{
  "data": {
    "total_clicks": 1500,
    "affiliate_clicks": 890,
    "affiliate_revenue": 3245.50,
    "by_merchant": {
      "amazon": { "clicks": 450, "revenue": 1820.00 },
      "ebay": { "clicks": 200, "revenue": 875.50 }
    },
    "periods": {
      "2026-10-06": { "clicks": 12, "affiliate_clicks": 5, "revenue": 125.00 },
      "2026-10-07": { "clicks": 0, "affiliate_clicks": 0, "revenue": 0.0 }
    }
  }
}

total_clicks counts every click. affiliate_clicks counts outbound clicks to merchants. revenue is the total price of the products clicked, at the time of the click. It is not commission or confirmed sales; the store cannot see what the merchant paid.

Clicks by Merchant

GET /api/analytics/clicks/by-merchant

Affiliate clicks only, over time. Takes the summary parameters plus:

Parameter Type Description
merchant string Part of a merchant name, e.g. amazon. Case-insensitive. Omit for all.
{
  "data": {
    "total": { "clicks": 890, "revenue": 3245.50 },
    "by_merchant": { "amazon": { "clicks": 450, "revenue": 1820.00 } },
    "periods": { "2026-10-07": { "clicks": 5, "revenue": 125.00 } }
  }
}

by_merchant is ordered by revenue, highest first. revenue has the same meaning as above.

Top Clicked Products

GET /api/analytics/clicks/top-products

Products with the most affiliate clicks.

Parameter Type Default Description
days integer 30 1 to 365
limit integer 10 1 to 50
{
  "data": {
    "days": 30,
    "products": [
      { "id": 41, "title": "Example Product", "path": "/products/example-product", "clicks": 37, "value": 1480.00 }
    ]
  }
}

value is the total price of the clicked product at click time. A deleted product shows as "Deleted product" with path: null.

List Clicks

GET /api/analytics/clicks

Every tracked click, newest first. Use ?affiliate=true for outbound merchant clicks only.

Parameter Type Description
affiliate boolean true for affiliate clicks only, false for internal only
product_id integer Filter by product
offer_id integer Filter by offer
from date Start date
to date End date
Field Type Description
id integer
product_id integer
offer_id integer
affiliate integer 1 = affiliate click, 0 = internal click
source string Page the click came from
destination string Merchant name (e.g. amazon)
url string Full destination URL
price float Product price at the time of the click
clicked_at datetime

Cart Checkouts Summary

GET /api/analytics/checkouts/summary

For stores with the cart turned on. Takes the summary parameters.

{
  "data": {
    "today": { "checkouts": 3, "revenue": 245.97 },
    "this_month": { "checkouts": 42, "revenue": 3891.50 },
    "total": { "checkouts": 580, "revenue": 52340.00 },
    "periods": { "2026-10-07": { "checkouts": 2, "revenue": 89.99 } }
  }
}

List Cart Checkouts

GET /api/analytics/checkouts

Newest first.

Parameter Type Description
from date Start date
to date End date
Field Type Description
id integer
action_type string Always cart_checkout
price float Total checkout value
properties object Cart details including cart_items
product_id integer
offer_id integer
performed_at datetime

List Actions

GET /api/analytics/actions

Every tracked visitor action, newest first.

Parameter Type Description
action_type string Filter by type (see below)
product_id integer Filter by product
from date Start date
to date End date

Action types: page_view, action, event, outlink, form, ecommerceOrder, ecommerceAbandonedCart, goal, crash, media, search, affiliate_click, cart_view, cart_add, cart_remove, cart_update, cart_checkout

Fields: id, action_type, product_id, offer_id, click_type, click_destination, click_url, search_keyword, price, properties, performed_at.


Data

Ability: data:read.

Get AI Token Usage

GET /api/data/openai-tokens

AI tokens used per month, newest first.

{ "data": { "2026-10": 4500, "2026-09": 3200 } }

Get Store Stats (alias)

GET /api/data/stats

Same as GET /api/store/stats.


All Endpoints

"Scoped key" is the resource ability a key needs when it holds any resource:verb ability. Every key also needs the broad read (for GET) or write (for the rest), unless it is Full Access.

Method Path Scoped key Description
GET /api/store/status none (public) Health check
GET /api/store/info store:read Store info
GET /api/store/instant-store-readiness store:read Content counts for copying a store
GET /api/store/stats store:read Content counts
GET /api/store/warnings store:read Store Health warnings
GET /api/store/locales store:read Enabled countries
GET /api/store/indexnow store:read IndexNow status
GET /api/store/messages/summary store:read Contact messages over time
POST /api/store/clean store:write Delete catalogue clutter
POST /api/store/clear-cache store:write Clear the store's caches and its CDN copy
POST /api/store/rebuild-sitemap store:write Rebuild the XML sitemap
POST /api/store/retire-catalogue store, products, categories, articles, brands, settings write Retire a catalogue and redirect its URLs
GET /api/store/retire-catalogue/{id} store, products, categories, articles, brands, settings read A retire's result
POST /api/store/regenerate-stock-texts store:write AI-rewrite template stock copy
POST /api/store/pick-logo-icon store:write AI-pick a logo icon
POST /api/store/footer-credit store:write Set the footer credit wording
POST /api/store/generate-logo store:write AI-generate a store logo
GET /api/tools/offer-health store:read Counts of what clean would delete
GET /api/redirects settings:read List redirects
POST /api/redirects settings:write Create redirect
POST /api/redirects/bulk settings:write Create up to 500 redirects
DELETE /api/redirects/{id} settings:write Delete redirect
GET /api/users users:read List users
POST /api/users users:write Create user
POST /api/users/invite platform only Invite an Admin
GET /api/users/login-link Full Access (*) only Magic login link
GET /api/users/{id} users:read Get user
PATCH /api/users/{id} users:write Update user
POST /api/users/{id}/invite platform only Resend invite
DELETE /api/users/{id} users:write Remove user
GET /api/settings settings:read All settings (credentials removed)
PATCH /api/settings settings:write Update settings (flat keys)
GET /api/settings/{group} settings:read Settings for one group (credentials removed)
PATCH /api/settings/{group} settings:write Update one group
GET /api/launch-plan settings:read Launch plan
POST /api/launch-plan/{key} settings:write Mark step done
DELETE /api/launch-plan/{key} settings:write Mark step not done
GET /api/recommendations store:read Recommendations
POST /api/member-facts platform only Push member facts (platform)
POST /api/recompute settings:write + store:read Re-check launch plan, return both
GET /api/products products:read List products
POST /api/products products:write Create product
GET /api/products/{id} products:read Get product
PATCH /api/products/{id} products:write Update product (not categories)
DELETE /api/products/{id} products:write Delete product
GET /api/products/{id}/offers products:read Product offers
GET /api/products/{id}/categories products:read Product categories
POST /api/products/{id}/categories products:write Set product categories (replace)
GET /api/products/{id}/analytics products:read Product analytics
POST /api/products/import-links products:write Import Amazon links
GET /api/offers/merchants offers:read Enabled merchants
GET /api/offers offers:read List offers
POST /api/offers offers:write Create offer
GET /api/offers/{id} offers:read Get offer
PATCH /api/offers/{id} offers:write Update offer
DELETE /api/offers/{id} offers:write Delete offer
POST /api/offers/{id}/products offers:write Link offer to products (add)
GET /api/categories/tree categories:read Category tree
GET /api/categories categories:read List categories
POST /api/categories categories:write Create category
GET /api/categories/{id} categories:read Get category
PATCH /api/categories/{id} categories:write Update category
DELETE /api/categories/{id} categories:write Delete category
GET /api/categories/{id}/products categories:read Category products
POST /api/categories/fill categories:write Fill categories with products
POST /api/categories/generate-list categories:write AI-suggest a category tree
POST /api/articles/{id}/images articles:write Set article image from a URL
GET /api/images/stock articles:read Search stock photos
GET /api/populators categories:read List Auto Populators
POST /api/populators/bulk categories:write Switch many on or off
GET /api/populators/{id} categories:read Get Auto Populator
PATCH /api/populators/{id} categories:write Update Auto Populator
GET /api/brands brands:read List brands
POST /api/brands brands:write Create brand
POST /api/brands/clean brands:write AI-clean brands
GET /api/brands/{id} brands:read Get brand
PATCH /api/brands/{id} brands:write Update brand
DELETE /api/brands/{id} brands:write Delete brand
GET /api/brands/{id}/products brands:read Brand products
GET /api/articles articles:read List articles
POST /api/articles articles:write Create article
POST /api/articles/generate-topics articles:write AI-suggest article topics
GET /api/articles/{id} articles:read Get article
PATCH /api/articles/{id} articles:write Update article
DELETE /api/articles/{id} articles:write Delete article
POST /api/articles/{id}/fill-empty-images articles:write Fill empty article images
GET /api/article-categories article-categories:read List article categories
POST /api/article-categories article-categories:write Create article category
GET /api/article-categories/{id} article-categories:read Get article category
PATCH /api/article-categories/{id} article-categories:write Update article category
DELETE /api/article-categories/{id} article-categories:write Delete article category
GET /api/pages pages:read List pages
POST /api/pages pages:write Create page
GET /api/pages/{id} pages:read Get page
PATCH /api/pages/{id} pages:write Update page
DELETE /api/pages/{id} pages:write Delete page
GET /api/analytics/visits/summary analytics:read Visits summary
GET /api/analytics/visits analytics:read List visits
GET /api/analytics/visitors analytics:read List visitors
GET /api/analytics/visitors/{id} analytics:read Get visitor
GET /api/analytics/pages analytics:read Page analytics
GET /api/analytics/countries analytics:read Country analytics
GET /api/analytics/devices analytics:read Device analytics
GET /api/analytics/sources analytics:read Source analytics
GET /api/analytics/sources/summary analytics:read Visitors over time by source
GET /api/analytics/clicks/summary analytics:read Clicks summary
GET /api/analytics/clicks/by-merchant analytics:read Affiliate clicks by merchant
GET /api/analytics/clicks/top-products analytics:read Most clicked products
GET /api/analytics/clicks analytics:read List clicks
GET /api/analytics/checkouts/summary analytics:read Cart checkouts summary
GET /api/analytics/checkouts analytics:read List cart checkouts
GET /api/analytics/actions analytics:read List actions
GET /api/data/openai-tokens data:read AI token usage
GET /api/data/stats data:read Content counts (alias)
Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.