FreshStore API Specification
Every store has an API that can be used to get data from or update your store. That includes products, settings, analytics, and more.
Please note that using an API requires some technical knowledge.
Fill Categories
Automatically fill categories with products from merchant APIs using AI-powered search. Jobs run in the background.
Note: Fill skips products that are currently out of stock at the merchant. This protects your store from importing items visitors cannot buy. To import out-of-stock or pre-order items on purpose, use the Link Importer or the Product Finder in the dashboard.
POST /api/categories/fill
| Field | Type | Required | Description |
|---|---|---|---|
categories |
integer[] | No | Array of category IDs (max 50). If omitted, all categories in the store will be used. |
api_name |
string | Yes | Merchant API: amazon, aliexpress, ebay, etsy, walmart |
locale |
string | Yes | Country locale (e.g. us, gb, de) |
quantity |
integer | No | Products per category (default: 5, max: 50) |
generate_content |
boolean | No | Generate AI content for imported products (default: true) |
ai_relevance_check |
boolean | No | Use AI to verify product relevance to category (default: true) |
Response 202: Jobs are queued in the background.
{
"data": {
"categories_queued": 3,
"quantity_per_category": 5,
"api_name": "amazon",
"locale": "us"
}
}
Import Links
Import products by pasting Amazon links or ASINs. Products are imported in the background.
Note: Import Links treats every link as an explicit choice. Out-of-stock and pre-order items are imported as requested. To clear out-of-stock items later, use the Clean Store endpoint with operations: ["out_of_stock_offers"].
POST /api/products/import-links
| Field | Type | Required | Description |
|---|---|---|---|
links |
string[] | Yes | Array of Amazon URLs or ASINs (max 100) |
categories |
integer[] | No | Category IDs to assign to imported products |
Response 202: Each link is validated and queued.
{
"data": {
"total": 3,
"importing": 2,
"duplicates": 1,
"failed": 0,
"links": [
{"link": "https://www.amazon.com/dp/B09V3KXJPB", "asin": "B09V3KXJPB", "locale": "us", "status": "importing"},
{"link": "B07XJ8C8F5", "asin": "B07XJ8C8F5", "locale": "us", "status": "importing"},
{"link": "https://www.amazon.com/dp/B08N5WRWNW", "asin": "B08N5WRWNW", "locale": "us", "status": "duplicate", "reason": "Product already exists", "product_id": 42}
]
}
}
Getting Started
1. Create an API Key
Go to Settings > API Keys in the store admin area and click Create API Key. Give it a name, choose the permissions you need, and optionally set an expiry date.
Once created, you'll see the API key once - copy it and store it securely. It cannot be retrieved again.
2. Authenticate Requests
Include your API key in the Authorization header of every request:
Authorization: Bearer YOUR_API_KEY
3. Permissions
When creating an API key, you can choose:
- Full Access: read and write everything
- Read / Write: broad read or write access across all resources
- Per-resource: granular access like "Products: Read", "Offers: Write", etc.
If a request exceeds the key's permissions, you'll receive a 403 Forbidden response.
Rate Limiting
All endpoints are rate-limited to 60 requests per minute.
WAF (Web Application Firewall) Approval
Your stores are protected by a WAF and common products/services are whitelisted. If you get a 403 response, check the following:
- User-Agent header: Your requests must include a valid browser-style User-Agent. Generic user-agents such as
curl,python-requests,axios, or empty User-Agent strings will be blocked. Use a standard browser User-Agent string, for example:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 - IP whitelisting: If you are still blocked after setting a valid User-Agent, contact support with your server IP to request a whitelist.
Both conditions must be met: a valid User-Agent and an approved IP (if your IP range is flagged).
Base URL
All of the endpoints listed below are accessible from your store url.
API Base URL: https://{your-store-domain}/
Response Format
Success
All responses wrap data in a data key:
{
"data": { ... }
}
List endpoints return paginated results:
{
"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
}
}
Errors
| Status | Meaning | Example Body |
|---|---|---|
| 401 | Unauthorized | "Unauthorized." |
| 403 | Forbidden | {"message": "This token does not have write access."} |
| 404 | Not Found | {"message": "The API resource was not found"} |
| 422 | Validation Error | {"message": "...", "errors": {"field": ["..."]}} |
| 500 | Server Error | {"message": "Server Error"} |
Pagination
All list endpoints accept these query parameters:
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
page |
integer | 1 | - | Page number |
per_page |
integer | 15 | 100 | Items per page |
Store
Get Status (Public)
GET /api/store/status
No authentication required. Returns whether the store is online.
{
"data": {
"up": true
}
}
Get Store Info
GET /api/store/info
{
"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
{
"data": {
"brands": 12,
"articles": 45,
"products": 150,
"categories": 23,
"offers": 320,
"articles_generated": 30
}
}
Also available at GET /api/data/stats .
Get Store Warnings
GET /api/store/warnings
{
"data": [
{
"message": "To get paid for your Amazon sales you need to add an Amazon Affiliate Tag",
"action": "Find Out More",
"url": "/admin/store-health"
}
]
}
Get Store Locales
GET /api/store/locales
Returns the enabled country/locale configurations.
{
"data": {
"us": { "name": "United States", "iso_3166_2": "US", "..." : "..." },
"gb": { "name": "United Kingdom", "iso_3166_2": "GB", "..." : "..." }
}
}
Clean Store
POST /api/store/clean
Removes expired offers, offerless products, empty brands, and empty categories. Optionally also removes out-of-stock offers. Mirrors the Store Cleaner tool in your store dashboard.
| Field | Type | Required | Description |
|---|---|---|---|
operations |
string[] | No | Any of: expired_offers, out_of_stock_offers, offerless_products, empty_brands, empty_categories. If omitted, the default set runs (expired_offers, offerless_products, empty_brands, empty_categories). out_of_stock_offers is opt-in only and must be requested explicitly. |
What each operation does
expired_offers: deletes offers that the merchant no longer lists in their API (the product has been removed or delisted entirely).out_of_stock_offers: deletes offers that are currently out of stock at the merchant. Opt-in only, because out-of-stock items can come back in stock at any time. Run this only when you want to clear those products from your store now.offerless_products: deletes products that have no offers attached.empty_brands: deletes brands with no products.empty_categories: deletes categories with no products and no children.
Response 200
{
"data": {
"deleted": {
"expired_offers": 12,
"offerless_products": 3,
"empty_brands": 1,
"empty_categories": 0
}
}
}
The response only contains keys for the operations that were requested. To run the out-of-stock cleanup, pass operations: ["out_of_stock_offers"] on its own or alongside other operations.
Regenerate Stock Texts
POST /api/store/regenerate-stock-texts
Rewrites a curated set of template text rows (homepage offer-box title, testimonial blurbs, about page intro, etc.) so the store stops carrying the generic "Bringing you the best products" defaults. Only touches rows that still hold the default value, so a customer's manual edits are never overwritten. One AI call per row, billed in AI Chips like the rest of the AI features.
No request body. The niche setting on the store (PATCH /api/settings/store with niche) feeds the prompt, so set the niche first if you want niche-specific copy.
Response 200
{
"data": {
"rewritten": 7,
"skipped_customised": 5,
"keys": [
"HOMEPAGE_OFFER_BOX_TITLE",
"HOMEPAGE_TESTIMONIAL_TEXT_1",
"HOMEPAGE_TESTIMONIAL_TEXT_2",
"HOMEPAGE_TESTIMONIAL_TEXT_3",
"ABOUT_PAGE_INTRO",
"ABOUT_PAGE_MISSION",
"HOMEPAGE_CONTENT_ABOVE_HERO"
]
}
}
Pick Logo Icon
POST /api/store/pick-logo-icon
Picks a niche-appropriate logo icon from the Crystal template's ~190 Material icons (the LOGO_ICON option) and writes it back to the store's template options. One AI call. The default shopping-bag icon looks generic on every store; this is the populate step that gives a brand-new store an icon that actually evokes its niche.
| Field | Type | Required | Description |
|---|---|---|---|
niche |
string | Yes | Free-text niche, e.g. indoor herb gardening, vintage cameras, cycling gear. |
Response 200 with one of these statuses:
picked: a new icon was chosen and saved. Response includes the icon key and human label.skipped: the store admin has already saved a non-default icon manually, left untouched.invalid_response: AI returned a key not in the catalogue; nothing saved. Safe to retry.no_option/no_choices: the LOGO_ICON template option is missing or empty (template not registered).
{
"data": {
"status": "picked",
"picked": "emoji-nature",
"label": "Bee"
}
}
Generate Logo
POST /api/store/generate-logo
Generates a wordmark logo image for the store: the store name rendered as stylised text, niche-appropriate in style and colour. The generated image is attached to the template's logo slot, so a brand-new store gets a real logo rather than the plain text fallback. The store name is read from the store's settings; it is not passed in the request.
| Field | Type | Required | Description |
|---|---|---|---|
niche |
string | Yes | Free-text niche, max 255 chars, e.g. indoor herb gardening, vintage cameras, cycling gear. Steers the logo's visual style. |
Response 200 with one of these statuses:
generated: a wordmark logo was created and attached to the template logo slot.skipped: nothing was generated. The response includes areasonof eitherlogo_already_set(the store already has a logo, left untouched) orno_store_name(the store has no name set, so there is no wordmark text to render).failed: generation did not complete. The response includes anerrormessage describing what went wrong. Safe to retry.
{
"data": {
"status": "generated"
}
}
Users
List Users
GET /api/users
Response: Paginated list.
{
"data": [
{
"id": 1,
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"roles": ["admin"]
}
]
}
Create User
POST /api/users
If a user with the given email already exists (including deleted users), the existing user is returned.
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Must be unique, max 255 chars |
password |
string | No | Random password generated if omitted |
first_name |
string | No |
|
last_name |
string | No |
|
roles |
string[] | No | Role names to assign |
newsletter |
boolean | No | Defaults to true |
Get Login Link
GET /api/users/login-link?email=jane@example.com
Generates a magic login link for the given user.
{
"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 |
|
last_name |
string | No |
|
email |
string | No |
|
Settings
Get All Settings
GET /api/settings
Returns all settings as flat key-value pairs using {group}_{setting} keys.
{
"data": {
"store_name": "My Fresh Store",
"store_niche": "tech",
"product_show_reviews": true,
"offers_amazon_enabled": true
}
}
Update All Settings
PATCH /api/settings
Send one or more settings using {group}_{setting} keys.
{
"store_name": "My Updated Store",
"product_show_reviews": false
}
Response:
{
"message": "Settings saved successfully."
}
Get Settings by Group
GET /api/settings/{group}
Available groups: store , offers , product , traffic , article , social , template , xml-sitemap
Returns settings for that group only, with keys without the group prefix.
{
"data": {
"name": "My Fresh Store",
"niche": "tech",
"live": true,
"default_country": "US"
}
}
Update Settings by Group
PATCH /api/settings/{group}
Available groups: store , offers , product , traffic , article , social , template , xml-sitemap
Send keys without the group prefix.
{
"name": "My Updated Store",
"niche": "tech"
}
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 |
array | Key features / bullet points |
summary |
string |
|
meta_title |
string |
|
meta_keywords |
string |
|
meta_description |
string |
|
review_quantity |
integer |
|
review_score |
float |
|
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 |
created_at |
datetime |
|
updated_at |
datetime |
|
brand |
object | Included via ?include=brand |
offers |
array | Included via ?include=offers |
categories |
array | Included via ?include=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 | Column to sort by (default: created_at ) |
sort_dir |
string | asc or desc (default: desc ) |
Create Product
POST /api/products
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Max 255 chars |
slug |
string | No |
|
subtitle |
string | No |
|
link_title |
string | No |
|
content |
string | No | HTML |
content_bottom |
string | No | HTML |
highlights |
array | 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 |
numeric | No |
|
enabled |
boolean | No |
|
in_sitemap |
boolean | No |
|
brand_id |
integer | No | Must exist in brands |
upc |
string | No |
|
ean |
string | No |
|
isbn |
string | No |
|
mpn |
string | No |
|
Response 201
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 provided fields are updated.
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" }]
}
Sync Product Categories
Replace all category associations for a product.
POST /api/products/{id}/categories
{
"category_ids": [1, 2, 3]
}
Get Product Analytics
GET /api/products/{id}/analytics
{
"data": {
"analytics_visits": 1250,
"total_clicks": 89,
"affiliate_clicks": 45
}
}
Offers
Offer Fields
| Field | Type | Description |
|---|---|---|
id |
integer |
|
uuid |
string | Unique merchant identifier |
merchant |
string | e.g. amazon , ebay , custom |
merchant_offer_id |
string | Merchant's product/ASIN ID, e.g. B0GR52QJCF for Amazon. |
merchant_product_id |
string | Deprecated alias for merchant_offer_id . Returns the same value. Will be removed in a future release. |
title |
string |
|
summary |
string |
|
features |
array |
|
currency |
string | e.g. USD , GBP |
price |
float |
|
price_full |
float | Original / list price |
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",
"etsy": "Etsy"
}
}
List Offers
GET /api/offers
| Parameter | Type | Description |
|---|---|---|
search |
string | Search by title |
merchant |
string | Filter by merchant name |
enabled |
boolean | Filter by enabled status |
product_id |
integer | Filter by product |
Create Offer
POST /api/offers
| Field | Type | Required | Description |
|---|---|---|---|
merchant |
string | Yes | Max 255 chars |
title |
string | Yes | Max 255 chars |
uuid |
string | No |
|
merchant_offer_id |
string | Yes | Merchant's product/ASIN ID, e.g. B0GR52QJCF for Amazon. Max 255 chars. |
merchant_product_id |
string | No | Deprecated alias for merchant_offer_id . If supplied, it is used as the offer id. Will be removed in a future release. |
summary |
string | No |
|
features |
array | No |
|
currency |
string | No | Max 10 chars |
price |
numeric | No |
|
price_full |
numeric | No |
|
min_price |
numeric | No |
|
max_price |
numeric | No |
|
condition |
string | No | Max 50 chars |
stock_available |
boolean | No |
|
enabled |
boolean | No |
|
locale |
string | No | Max 10 chars |
images |
array | No |
|
url_detail |
string | Yes | Full URL to the product on the merchant site. |
Example body
{
"merchant": "amazon",
"merchant_offer_id": "B0GR52QJCF",
"title": "Amazon Product",
"url_detail": "https://www.amazon.com/dp/B0GR52QJCF"
}
To attach the new offer to one or more existing products, follow up with a POST /api/offers/{id}/products call.
Response 201
Get Offer
GET /api/offers/{id}
Update Offer
PATCH /api/offers/{id}
Same fields as Create. All optional.
Delete Offer
DELETE /api/offers/{id}
Response 204 (no content).
Sync Offer Products
Replace all product associations for an offer.
POST /api/offers/{id}/products
{
"product_ids": [1, 2, 3]
}
Categories
Category Fields
| Field | Type | Description |
|---|---|---|
id |
integer |
|
parent_id |
integer | Parent category (null = root) |
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 |
|
children_count |
integer |
|
created_at |
datetime |
|
updated_at |
datetime |
|
Get Category Tree
GET /api/categories/tree
Returns all categories as a nested tree structure.
{
"data": [
{
"id": 1,
"title": "Electronics",
"children": [
{
"id": 5,
"title": "Phones",
"children": []
}
]
}
]
}
List Categories
GET /api/categories
| Parameter | Type | Description |
|---|---|---|
search |
string | Search by title |
enabled |
boolean | Filter by enabled status |
parent_id |
integer | Filter by parent (use empty string for root only) |
Create Category
POST /api/categories
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Max 255 chars |
slug |
string | No |
|
parent_id |
integer | No | Must exist in categories |
subtitle |
string | No |
|
link_title |
string | No |
|
content_top |
string | No |
|
content_bottom |
string | No |
|
meta_title |
string | No |
|
meta_keywords |
string | No |
|
meta_description |
string | No |
|
enabled |
boolean | No |
|
sort_order |
integer | No |
|
generate_ai_content |
boolean | No | If true, dispatches a background AI job to fill the subtitle, link title, top + bottom content, and meta fields after create. Chips billed via the standard RequestContent flow. |
generate_images |
boolean | No | If true, dispatches a background job to populate the category banner + listing image. See image_source for how the images are sourced. |
image_source |
string | No | auto (default), stock, or ai. Controls how generate_images resolves. See the Image source section below. |
Response 201
Generate Category List (AI)
POST /api/categories/generate-list
AI-suggest a structured category tree for the store's niche. Returns the proposed tree as JSON; the caller is expected to then POST /api/categories for each one (optionally with generate_ai_content / generate_images). Chips are billed once for the AI call via RequestContent.
| Field | Type | Required | Description |
|---|---|---|---|
niche |
string | Yes | The store's niche (e.g. "vintage cameras") |
max_categories |
integer | No | 1 to 20, default 5 |
max_subcategories |
integer | No | 0 to 10, default 5 |
Every main category in the response always comes back with 3 to 5 subcategories; the AI never returns a main category with an empty subcategory list.
Response 200
{
"data": {
"niche": "vintage cameras",
"categories": [
{
"title": "Film SLRs",
"subcategories": ["35mm", "Medium Format", "Manual Focus"]
},
{
"title": "Rangefinders",
"subcategories": ["Fixed Lens", "Interchangeable Lens"]
}
]
}
}
If the AI call fails, returns 503 with the standard error envelope.
Get Category
GET /api/categories/{id}
Update Category
PATCH /api/categories/{id}
Same fields as Create. All optional.
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.
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 |
|
created_at |
datetime |
|
updated_at |
datetime |
|
List Brands
GET /api/brands
| 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 |
|
subtitle |
string | No |
|
link_title |
string | No |
|
content |
string | No |
|
content_bottom |
string | No |
|
meta_title |
string | No |
|
meta_keywords |
string | No |
|
meta_description |
string | No |
|
enabled |
boolean | No |
|
Response 201
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.
Clean Brands
POST /api/brands/clean
AI-classifies every brand on the store and applies the verdict: real, niche-relevant brands are title-cased, enabled, and queued for AI content fill; gibberish / single-product OEMs are deleted; borderline brands are left disabled. Also strips broken "Brand: Foo, Inc." prefixes that occasionally come in from Amazon data. One batched AI call, idempotent, safe to re-run after more products import.
| Field | Type | Required | Description |
|---|---|---|---|
niche |
string | No | Free-text niche. Strongly recommended: the prompt makes niche-fit the primary gate, so a globally-famous brand from a wrong category (e.g. GoPro on an herb-gardening store) is deleted or kept disabled even if it has many products attached. |
keep_top |
integer | No | 1 to 50. When provided, caps how many brands stay enabled. After the AI classification, only the top keep_top brands by product count among the brands the AI verdicted as "enable" remain enabled; any further "enable" brands are left disabled (not deleted). Omit to enable every brand the AI approves. |
Response 200
The enabled count is the post-cap count (how many brands ended up enabled). When keep_top is set, capped reports how many brands the AI wanted to enable but were held back by the cap; those brands are left disabled. Without keep_top, capped is 0.
{
"data": {
"classified": 38,
"enabled": 6,
"capped": 4,
"deleted": 22,
"kept_disabled": 10,
"title_fixes": 3
}
}
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 |
published_at |
datetime |
|
created_at |
datetime |
|
updated_at |
datetime |
|
article_categories |
array | Included on single get |
List Articles
GET /api/articles
| 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 | Restrict to articles attached to the given article category id |
Create Article
POST /api/articles
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Max 255 chars |
slug |
string | No |
|
subtitle |
string | No |
|
link_title |
string | No |
|
content |
string | No |
|
meta_title |
string | No |
|
meta_keywords |
string | No |
|
meta_description |
string | No |
|
enabled |
boolean | No |
|
published_at |
date | No | ISO 8601 |
product_category_id |
integer | No | ID of the product category used for related products on this article |
article_categories |
integer[] | No | Array of article category IDs to assign this article to |
generate_ai_content |
boolean | No | Set to true to generate AI content using the title as the topic |
generate_seo_keyword |
string | No | SEO keyword to target in the generated content (max 255 chars) |
generate_images |
boolean or string[] | No | Generate images for the article. Pass true to generate all three (listing, hero, social). Pass an array such as ["hero"] to generate only specific types. Accepted values: listing, hero, social. Works with or without generate_ai_content. See image_source below for how the images are sourced. |
image_source |
string | No | auto (default), stock, or ai. Controls how generate_images resolves. See the Image source section below. |
Response 201
When generate_ai_content is true, the article title is used as the AI generation topic. Content, metadata, and optionally images are generated in the background. The article is returned immediately with a 201 status and the generated content will populate once the background job completes.
Get Article
GET /api/articles/{id}
Includes article_categories .
Update Article
PATCH /api/articles/{id}
Same fields as Create. All optional.
Delete Article
DELETE /api/articles/{id}
Response 204 (no content).
Fill Empty Images
POST /api/articles/{id}/fill-empty-images
Copies the article's Hero Image into the Listing and Social image slots when those slots are empty. Mirrors the Fill Empty Images checkbox on the article edit page. Existing images are never overwritten. If the article has no Hero Image set, the call is a no-op.
This is useful for saving AI Chips: generate only the Hero Image on article creation (generate_images: ["hero"], ~50k chips) and then call this endpoint to populate the Listing and Social slots for free.
Response 200
{
"data": {
"filled": ["listingImage", "socialImage"]
}
}
The filled array lists which image collections were populated by this call. An empty array means either the Hero Image was not set, or both slots were already populated.
Generate Article Topics (AI)
POST /api/articles/generate-topics
Generate a batch of niche-appropriate article topic suggestions for the store. Each topic is one AI call so chips are billed N times. The suggestions are intended as inputs to POST /api/articles (with generate_ai_content: true to write the article itself).
| Field | Type | Required | Description |
|---|---|---|---|
count |
integer | No | 1 to 10, default 3 |
Response 200
{
"data": {
"topics": [
"Beginners Guide to Buying Your First Film Camera",
"Caring for Vintage Camera Leather and Bellows",
"How to Spot a Rangefinder That\'s Worth the Price"
],
"requested": 3,
"returned": 3
}
}
returned may be less than requested if the AI returned a duplicate suggestion or an empty result for one of the calls.
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 |
|
created_at |
datetime |
|
updated_at |
datetime |
|
List Article Categories
GET /api/article-categories
| 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 |
|
parent_id |
integer | No | Must exist in article categories |
subtitle |
string | No |
|
link_title |
string | No |
|
content_top |
string | No |
|
content_bottom |
string | No |
|
meta_title |
string | No |
|
meta_keywords |
string | No |
|
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).
Image source (stock vs AI)
Endpoints that generate images (POST /api/categories, POST /api/articles, plus the internal GenerateImages job used by other admin flows) accept an image_source parameter that controls how each image is sourced.
| Mode | Behaviour | Chip cost |
|---|---|---|
auto |
Default. Try Pexels, then Unsplash, then Pixabay. Fall back to OpenAI only if all three miss for that image. | A few hundred chips per stock-resolved image (for the AI keyword call); ~50,000 per AI fallback. |
stock |
Stock providers only. Skip the image if none match. No AI fallback. | A few hundred chips per attempt (keyword call only). |
ai |
Skip stock entirely. Generate every image with OpenAI. Legacy behaviour. | ~50,000 chips per image. |
How auto resolves
For each image collection (banner, listing, hero, social):
- A short 2 to 5 word search term is built from the entity title, the store's
nichesetting, and the target collection (banner vs square thumbnail vs hero). Single small AI text call. - The store queries Pexels first, then Unsplash, then Pixabay, stopping at the first hit. Providers without an API key are skipped silently.
- On hit, the image is downloaded and attached to the entity's media collection. Indistinguishable from an AI-generated image downstream (same disks, conversions, attribution metadata).
- On total miss in
automode, the existing OpenAI image flow runs.
Stock provider API keys
Set per store via the store settings group (or via env vars on a fresh store): pixabay_api_key, pexels_api_key, unsplash_api_key. Stores without any configured keys behave as if image_source were ai, since every provider attempt is skipped.
Attribution
Stock images carry the source provider and photographer in their media record's custom_properties.attribution. Templates may display this on the rendered page if a stock provider's terms require it (Unsplash attribution is recommended; Pexels and Pixabay are optional).
Pages
Page Fields
| Field | Type | Description |
|---|---|---|
id |
integer |
|
key |
string | Stable identifier for system pages (e.g. homepage, about, contact). Null for custom 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
| Parameter | Type | Description |
|---|---|---|
search |
string | Search by title |
key |
string | Exact match on key. e.g. ?key=homepage returns the auto-created homepage page. |
enabled |
boolean | Filter by enabled status |
Create Page
POST /api/pages
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Max 255 chars |
key |
string | No | Max 255 chars. Stable identifier for system pages, e.g. about or homepage. Leave unset for ordinary custom pages. |
slug |
string | No |
|
subtitle |
string | No |
|
link_title |
string | No |
|
content |
string | No |
|
meta_title |
string | No |
|
meta_keywords |
string | No |
|
meta_description |
string | No |
|
enabled |
boolean | No |
|
generate_ai_content |
boolean | No | If true, dispatches a background AI job to fill the title, subtitle, meta, and the homepage hero content block. Chips billed via the standard RequestContent flow. |
Response 201
Get Page
GET /api/pages/{id}
Update Page
PATCH /api/pages/{id}
Same fields as Create, all optional. Two extra flags trigger background AI work on the page:
| Field | Type | Required | Description |
|---|---|---|---|
generate_ai_content |
boolean | No | When true, AI fills the page's title, subtitle, and meta in the background. For the homepage page it also generates the hero content block, the offer box texts, and the homepage section heading texts. For a non-homepage content page it also generates the page body content. Useful on the auto-created homepage page of a fresh store. |
generate_images |
boolean | No | When true, the page's image collections are populated in the background. For the homepage page this generates the hero image (TEMPLATE_IMAGE_HOMEPAGE_HERO, 1536x1024), the offer box background image, and the social / OG image. Pages other than homepage have no wired image collections and no-op. |
image_source |
string | No | Image-source mode for generate_images: auto (default, try stock first then AI), stock (stock only, skip on miss), or ai (AI only). See the Image source section above. |
Delete Page
DELETE /api/pages/{id}
Response 204 (no content).
Analytics
Get Visits Summary
GET /api/analytics/visits/summary
| Parameter | Type | Default | Description |
|---|---|---|---|
period |
string | day |
day , week , or month |
length |
integer | 30 | Number of periods to return |
{
"data": {
"2026-03-06": { "nb_uniq_visitors": 45, "nb_visits": 52, "nb_actions": 120 },
"2026-03-05": { "..." }
}
}
List Visits
GET /api/analytics/visits
| 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 |
Returns paginated visit records with 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
Returns paginated visitor records with 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
{
"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
{
"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
{
"data": [
{
"id": 1,
"device": "Desktop",
"visitors": 300,
"visits": 400,
"actions": 1200,
"visit_duration_seconds": 30000
}
]
}
Get Sources Analytics
GET /api/analytics/sources
{
"data": [
{
"id": 1,
"source": "Google",
"visitors": 200,
"visits": 250,
"actions": 600,
"visit_duration_seconds": 18000
}
]
}
Affiliate Clicks Summary
Revenue and click counts for affiliate link clicks, with a breakdown by merchant and per-period time series.
GET /api/analytics/clicks/summary
| Parameter | Type | Default | Description |
|---|---|---|---|
period |
string | day |
day , week , or month |
length |
integer | 30 | Number of periods to return |
{
"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 },
"etsy": { "clicks": 240, "revenue": 550.00 }
},
"periods": {
"2026-03-07": { "clicks": 12, "affiliate_clicks": 5, "revenue": 125.00 },
"2026-03-08": { "clicks": 8, "affiliate_clicks": 3, "revenue": 75.50 },
"2026-03-09": { "clicks": 0, "affiliate_clicks": 0, "revenue": 0.0 }
}
}
}
total_clicks includes all clicks (affiliate and non-affiliate). affiliate_clicks and affiliate_revenue are for affiliate clicks only. The periods key contains a time-series breakdown. Periods with no activity are included as zeros.
List Clicks
Returns all tracked clicks, both affiliate outbound clicks and internal clicks. Use ?affiliate=true to get only affiliate clicks (outbound clicks to merchants like Amazon, eBay, etc.).
GET /api/analytics/clicks
Parameters:
| Parameter | Type | Description |
|---|---|---|
product_id |
integer | Filter by product |
offer_id |
integer | Filter by offer |
affiliate |
boolean | Filter affiliate clicks only |
from |
date | Start date |
to |
date | End date |
Each click record includes:
| Field | Type | Description |
|---|---|---|
id |
integer |
|
product_id |
integer |
|
offer_id |
integer |
|
affiliate |
integer | 1 = affiliate click, 0 = internal click |
source |
string | Page/URL the click originated from |
destination |
string | Merchant name (e.g. amazon , ebay ) |
url |
string | Full destination URL |
price |
float | Product price at time of click |
clicked_at |
datetime |
|
Cart Checkouts Summary
Revenue and checkout counts, broken down by today, this month, all time, and per-period time series.
GET /api/analytics/checkouts/summary
| Parameter | Type | Default | Description |
|---|---|---|---|
period |
string | day |
day , week , or month |
length |
integer | 30 | Number of periods to return |
{
"data": {
"today": { "checkouts": 5, "revenue": 245.00 },
"this_month": { "checkouts": 120, "revenue": 6500.00 },
"all_time": { "checkouts": 3400, "revenue": 185000.00 },
"periods": {
"2026-03-07": { "checkouts": 3, "revenue": 150.00 },
"2026-03-08": { "checkouts": 2, "revenue": 95.00 }
}
}
}
List Cart Checkouts
Paginated list of individual cart checkout records.
GET /api/analytics/checkouts
| Parameter | Type | Description |
|---|---|---|
from |
date | Start date |
to |
date | End date |
Each checkout record includes:
| Field | Type | Description |
|---|---|---|
id |
integer |
|
session_id |
string |
|
items |
array | Products in the checkout |
total |
float | Cart total |
currency |
string |
|
country |
string | Shopper country |
created_at |
datetime |
|
List Actions
Paginated list of tracked user actions (page views, events, etc.).
GET /api/analytics/actions
| Parameter | Type | Description |
|---|---|---|
visit_id |
integer | Filter by visit |
visitor_id |
integer | Filter by visitor |
type |
string | Action type (e.g. pageview , event ) |
Each action record includes: id , visit_id , visitor_id , type , url , title , time_spent_seconds , created_at .
AI Token Usage
GET /api/data/openai-tokens
{
"data": {
"tokens_used": 125000,
"tokens_limit": 500000,
"tokens_remaining": 375000,
"resets_at": "2026-04-01"
}
}
Quick Reference
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/store/status |
Store online status (public) |
GET |
/api/store/info |
Store details |
GET |
/api/store/stats |
Content counts |
GET |
/api/store/warnings |
Active warnings |
GET |
/api/store/locales |
Enabled countries |
POST |
/api/store/clean |
Run store cleaner |
POST |
/api/store/regenerate-stock-texts |
AI-rewrite default template texts |
POST |
/api/store/pick-logo-icon |
AI-pick niche-fit logo icon |
POST |
/api/store/generate-logo |
AI-generate a wordmark logo image |
GET |
/api/users |
List users |
POST |
/api/users |
Create user |
GET |
/api/users/login-link |
Magic login link |
GET |
/api/users/{id} |
Get user |
PATCH |
/api/users/{id} |
Update user |
GET |
/api/settings |
All settings |
PATCH |
/api/settings |
Update settings |
GET |
/api/settings/{group} |
Settings by group |
PATCH |
/api/settings/{group} |
Update group settings |
GET |
/api/products |
List products |
POST |
/api/products |
Create product |
GET |
/api/products/{id} |
Get product |
PATCH |
/api/products/{id} |
Update product |
DELETE |
/api/products/{id} |
Delete product |
GET |
/api/products/{id}/offers |
Product offers |
GET |
/api/products/{id}/categories |
Product categories |
POST |
/api/products/{id}/categories |
Sync product categories |
GET |
/api/products/{id}/analytics |
Product analytics |
GET |
/api/offers/merchants |
Enabled merchants |
GET |
/api/offers |
List offers |
POST |
/api/offers |
Create offer |
GET |
/api/offers/{id} |
Get offer |
PATCH |
/api/offers/{id} |
Update offer |
DELETE |
/api/offers/{id} |
Delete offer |
POST |
/api/offers/{id}/products |
Sync offer products |
GET |
/api/categories/tree |
Category tree |
GET |
/api/categories |
List categories |
POST |
/api/categories |
Create category |
GET |
/api/categories/{id} |
Get category |
PATCH |
/api/categories/{id} |
Update category |
DELETE |
/api/categories/{id} |
Delete category |
GET |
/api/categories/{id}/products |
Category products |
GET |
/api/brands |
List brands |
POST |
/api/brands |
Create brand |
GET |
/api/brands/{id} |
Get brand |
PATCH |
/api/brands/{id} |
Update brand |
DELETE |
/api/brands/{id} |
Delete brand |
GET |
/api/brands/{id}/products |
Brand products |
POST |
/api/brands/clean |
AI-classify and clean brands |
GET |
/api/articles |
List articles |
POST |
/api/articles |
Create article |
GET |
/api/articles/{id} |
Get article |
PATCH |
/api/articles/{id} |
Update article |
DELETE |
/api/articles/{id} |
Delete article |
POST |
/api/articles/{id}/fill-empty-images |
Fill empty article images |
GET |
/api/article-categories |
List article categories |
POST |
/api/article-categories |
Create article category |
GET |
/api/article-categories/{id} |
Get article category |
PATCH |
/api/article-categories/{id} |
Update article category |
DELETE |
/api/article-categories/{id} |
Delete article category |
GET |
/api/pages |
List pages |
POST |
/api/pages |
Create page |
GET |
/api/pages/{id} |
Get page |
PATCH |
/api/pages/{id} |
Update page |
DELETE |
/api/pages/{id} |
Delete page |
GET |
/api/analytics/visits/summary |
Visits summary |
GET |
/api/analytics/visits |
List visits |
GET |
/api/analytics/visitors |
List visitors |
GET |
/api/analytics/visitors/{id} |
Get visitor |
GET |
/api/analytics/pages |
Page analytics |
GET |
/api/analytics/countries |
Country analytics |
GET |
/api/analytics/devices |
Device analytics |
GET |
/api/analytics/sources |
Source analytics |
GET |
/api/analytics/clicks/summary |
Affiliate clicks summary |
GET |
/api/analytics/clicks |
List clicks |
GET |
/api/analytics/checkouts/summary |
Cart checkouts summary |
GET |
/api/analytics/checkouts |
List cart checkouts |
GET |
/api/analytics/actions |
List actions |
GET |
/api/data/openai-tokens |
AI token usage |
GET |
/api/data/stats |
Content counts (alias) |
POST |
/api/products/import-links |
Import products from Amazon links |
POST |
/api/categories/fill |
Fill categories with products |
POST |
/api/categories/generate-list |
AI-suggest category tree for a niche |
POST |
/api/articles/generate-topics |
AI-suggest article topics |