# Billing & Plans
Source: https://docs.frontic.com/admin/billing-and-plans
Each [team](/admin/teams) is on a single plan. The plan determines which features the team can use, the usage allowance the team consumes each billing period, and the invoice cadence.
For the current pricing, see the [Frontic pricing page](https://www.frontic.com/pricing) — it's the source of truth for what each tier costs.
## Plan tiers
The free starter tier — get a project running, ingest data, build
a typed API, ship a frontend. Suitable for small projects and early
prototypes. Unlimited users, projects, and API endpoints.
Pay-as-you-go. Everything in Launch plus AI Frontend Studio. Above
the included allowance, additional usage is billed per unit as it's
consumed.
Everything in Grow plus [Release Control](/releases/overview) for
staged rollouts and Preview Releases. Available monthly or yearly.
Enterprise tier with custom limits, dedicated support, and bespoke
commercial terms. [Get in touch](mailto:support@frontic.com)
to discuss.
## Plan-gated features
A few capabilities sit behind specific tiers:
* **AI Frontend Studio** — Grow, Plus, and Scale. Launch ships without Studio.
* **Release Control** — staged previews and promote-to-public flows. Plus and Scale.
* **Context Documents** — AI Studio's Skills, Rules, Commands, and Guides authoring surface. Plus and Scale.
## Usage
Three metrics meter your team's monthly consumption. Each plan ships with an included allowance; usage above the allowance follows the plan's overage rules.
* **API Records** — a snapshot of how many records currently exist across your project's Blocks. The total is sampled regularly; whichever records are present at sampling time contribute, whether or not anything is reading them. To bring this down, delete records you no longer need.
* **API Updates** — ingest events delivered to your project: upsert and patch records arriving via the [Ingest API](/reference/ingest-api) or pulled by a Data Feed run. Deletes don't count.
* **API Requests** — read activity from your frontends and tooling: every call to a Block, Listing, Page, Tree, or Context endpoint counts as one request.
### Quotas
| Metric | Launch | Grow | Plus | Scale |
| ------------ | ------ | ------------------- | ---- | ------ |
| API Records | 10K | 10K + pay-per-unit | 100K | Custom |
| API Updates | 100K | 100K + pay-per-unit | 100M | Custom |
| API Requests | 100K | 100K + pay-per-unit | 5M | Custom |
### What counts against the quota
[Release Control](/releases/overview) gives you `develop`, `preview`, and `public` stages running side by side. Each metric handles them differently:
* **Records** are counted from `public` whenever a project has been promoted, otherwise from `develop`. **`preview` is excluded** — running multiple Preview Releases never inflates your record count.
* **Updates** are counted once per inbound record update, not once per stage. The three stages each process the record afterwards, but the meter only ticks at the inbound point.
* **Requests** are counted per Fetch API call, scoped to the team. Each stage has its own version token, so calls to `develop`, `preview`, and `public` are distinct requests and each one counts. A frontend hitting `public` plus a CI job hitting `preview` is two requests against the meter.
The Billing page in the admin app (**Settings → Billing**) shows the team's current-period consumption, so you can see where you sit against the allowance.
## Who can change the plan
Contact us to change your plan or discuss any custom requirements.
## Related
The staged-rollout system available for Plus and Scale plans.
Current per-tier pricing on the Frontic website.
Plans are per team; switching teams switches plans.
# Limits
Source: https://docs.frontic.com/admin/limits
To ensure a consistent performance for all your projects, Frontic enforces API limits.
Whenever you think you might hit or exceed a limit, please talk to us - we may be able to increase some of them for you.
## General
If you exceed a limit, we will notify you.
Any actions associated with exceeding the limit may fail and show an error message. If you see such an error message, you can contact us directly (e.g. via the support widget).
Any fields you define can have a field name of up to **100 characters** long.
* A maximum of **20 projects** can be created per team.
* A maximum of **50 members** can be added to your team.
### Integrations
When importing data into Frontic, a couple of limits must be observed, depending on your Integration type.
#### Ingest
If you're pushing data to Frontic, you must split your data into a maximum of **100 records per request**.
A single request can contain a payload with a maximum of **5 Megabytes**.
#### Trigger
If you're using an Integration that fetches data from your source, we will only process data that is smaller than **5 Megabytes** per request. For our included integrations, we'll handle correct pagination to reach that limit.
You can trigger a refresh of a maximum of **100.000 identifiers** per request.
Your data source must establish our connection attempt within **1 second** and return a response within **5 seconds**.
### Feed Records
A feed record is a single record from a connected feed, like an integration with your e-commerce system or your PIM.
It must not be larger than **300 Kilobytes**.
### Storage Records
A Storage Record is a single record that is created by mapping a Feed Record via a Data Sync.
It must not be larger than **20 Kilobytes**.
In total, you can create up to **50.000.000 Storage Records** per Storage.
When mapping your Storage Records, your mapping logic can be nested to a maximum of **3 levels** deep.
### Projects
In your projects, there are some configuration objects.
* A maximum of **20 locales** can be created per project.
* A maximum of **20 regions** can be created per project.
* A maximum of **5 scopes** can be created per project.
#### Data Types
You can create custom composite and enum data types in your Frontic Project.
* A maximum of **20 composite types** can be created per data type.
* Each composite type can have a maximum of **30 composite fields**.
* Composite fields can be nested to a maximum of **3 levels**.
* A maximum of **20 enum types** can be created per data type.
* Each enum type can have a maximum of **10 enum keys**.
### API Builder
A maximum of **50 blocks** and **50 listings** can be created per project.
#### Blocks
You can add up to **100 fields** to a block.
The number of blocks you can nest inside another block is limited to **10 nested block fields** per block.
#### Listings
Listings have a variety of options to configure the actual query being performed.
* You can use a maximum of **20 filter fields** per listing. Each filter will return a maximum of **50 facets**.
* You can use a maximum of **10 search fields** per listing.
* You can use a maximum of **10 sort fields** per listing.
* You can use a maximum of **10 query conditions** per listing.
* You can use a maximum of **10 block parameters** per listing.
* You can get a maximum of **48 result records per page** when using a listing. The number of total pages is only limited by the number of records in your storage.
### Pages
A maximum of **20 pages** can be created per project.
# Members & Roles
Source: https://docs.frontic.com/admin/members-and-roles
A **member** is a person with access to a [team](/admin/teams). Members are invited by email and assigned a **role** that controls what they can do across the team and its projects. Member access is per-team — adding someone to one team doesn't give them access to another.
## Roles
Frontic has three roles:
Full control of the team — can invite or remove
any member and transfer or delete the team. There's always at least
one Owner.
Full project access plus member management. Can invite Members
and Admins, edit project configuration, publish releases. Can't
remove the team.
Access to the projects in the team — view, edit, and use Studio
to ship changes. Can't manage other members.
Roles apply at the team level — a Member is a Member of every project in the team. Per-project access scoping isn't a separate dial; if someone shouldn't see one of your projects, they shouldn't be in the team.
## Permissions
Members get a read-only baseline across every project surface — view storages, blocks, pages, releases, Studio jobs, Context Base documents, and so on. Anything that **changes state** is gated to Admin or Owner. The two tables below highlight what each higher tier unlocks over the one below.
### What Admin adds over Member
Day-to-day building, shipping, and managing the project:
| Area | What Admin can do |
| ----------------- | ------------------------------------------------------------------ |
| API Builder | Create, edit, delete Blocks, Listings, and Page routes |
| Data Storages | Create, edit, delete storages |
| Data Types | Manage composites and enums |
| Domains & Locales | Add and remove domains; add locales (delete is Owner-only) |
| Data Syncs | Configure mappings (delete is Owner-only) |
| Integrations | Connect new integrations (disconnect is Owner-only) |
| Releases | Publish previews, promote to public |
| Studio | Full access — chat, jobs, context documents, credentials, settings |
| Context Base | Author Skills/Rules/Commands/Guides; promote to team-global scope |
| API Keys | View and create Ingest/Fetch keys (delete is Owner-only) |
| Project | Edit project name and description |
| Members | Invite Members and Admins; change non-Owner roles |
### What Owner adds on top of Admin
Team-level governance and one-way destructive actions:
| Area | Owner-only |
| ----------------- | ----------------------------------------------------------------------------------------- |
| Billing & plan | View billing & plan details |
| Team settings | Rename or delete the team |
| Member governance | Promote members to Owner; remove members |
| Destructive ops | Disconnect integrations, remove locales, delete API keys, delete data sync configurations |
## Inviting members
Open **Settings → Users** in the admin app and add invitations by email. Each invitation carries:
* **Email address** — where the invitation goes.
* **Role** — what the new member can do once they accept.
The recipient gets an email with a sign-in link. New members appear in the Users list once they accept.
## Changing a role
From the Users list, open the row for the member you want to change and pick a different role. The change applies on the member's next request to the admin app — no re-login needed.
A few rules:
* An Owner can promote anyone, including other members to Owner. Multiple Owners are fine.
* The team always needs at least one Owner — the last Owner can't be demoted or removed without first promoting another member.
* Admins can manage Members and other Admins, but can't promote anyone to Owner or remove Owners.
## Removing a member
Removing a member revokes their access to the team and every project in it. Their work history (commits, agent sessions, configuration changes) stays attributed to them — removal is about access, not erasure.
## Related
The container members belong to.
Plan tiers also gate which features each member can reach.
Buddy operates with the role of the member currently signed in —
a Member's Buddy can't make changes the Member can't.
The OAuth token for an editor MCP carries the same role as the
member who authorised it.
# Teams
Source: https://docs.frontic.com/admin/teams
A **team** is the top-level container in Frontic — it owns your projects and the resources every project draws on, plus the people working in it and the billing relationship. Most companies have one team; agencies, large groups, and teams running fully separate engagements run more than one. Switching between them is a one-click action in the user menu (top-right).
## What lives at the team level
The team is the boundary for several things every project inside it shares. When you're inside a project, these are already there for you to use:
The people with access to the team and every project in it. Invited
once, available across all projects. See [Members & Roles](/admin/members-and-roles).
[Connectors](/connectors/overview) are set up at the team level.
Once an integration is connected, every project can pull from
its Data Feeds.
[Skills, Rules, Commands, and Guides](/context-base/overview)
promoted to the team are available in every project. Project-scoped
documents stay project-only.
One [plan](/admin/billing-and-plans) per team, one invoice. Plan
limits and feature gates apply across every project the team owns.
Projects, by contrast, hold the stack itself — Data Storages, Blocks, Listings, Pages, project-scoped context documents, and project-level settings.
## When to add another team
Most setups don't need a second team — and adding one comes with a real cost: you re-set-up the team-level resources above (members re-invited, integrations re-connected, global context re-authored or re-promoted). The reasons that justify paying that cost:
A client engagement that pays its own bill, an internal cost-centre
split, an acquisition you haven't merged yet. Different team =
different invoice.
Two groups of people that shouldn't see each other's projects or
integrations. Member access is per-team, so a clean split is the
simplest answer.
You're an agency running multiple clients in Frontic. One team per
client keeps projects, members, integrations, and billing isolated.
If you want to **experiment** on something without risking a production project, the right move is a new project inside the same team — not a new team. Same integrations, same members, same context — clean separation at the project level.
## Switching between teams
The team picker lives in the user menu (top-right of the admin app). Click your avatar, pick a team, and the admin app switches context — including the project list, the members view, the integrations list, and the billing screen. Your account can belong to as many teams as you've been invited to; teams stay independent (different integrations, different members, different billing) even when you're in both.
## Creating a team
A team is created when you sign up — your account starts with one. Adding another team later is done from the same user menu and walks you through name, plan selection, and the first project.
## Related
Invite people to the team and pick what they can do.
Plan tiers, feature gates, and how billing per team works.
Plan limits that apply to every project in the team.
Per-project configuration — sits below the team layer.
# Detail Blocks
Source: https://docs.frontic.com/api-builder/blocks
A Detail Block is an API endpoint with a user-defined schema — it returns the exact data one UI component needs. Blocks are reusable, have parameters, and can be nested inside each other.
Manage your blocks in the [API Builder](https://app.frontic.com/builder/blocks) section of the admin app.
## Create a block
Head to the [API Builder](https://app.frontic.com/builder/blocks) and click ** Create Block**.
Frontic generates an endpoint URL as you type the name.
Block names must use normal capitalized words with spaces (e.g., `Product Card`, not `ProductCard`). Frontic derives the namespace and endpoint slug from this value — incorrect casing will break the auto-generated endpoint.
### Description
Add a description for your block. This is used for the auto-generated API documentation.
### Storage connection
Connect your block to a [Data Storage](/data-integration/data-storages) to populate it with real data.
For commerce storages, you choose whether the block operates on the **product level** or the **variant level**. This determines which fields are available in the response schema:
* **Product-level blocks** access shared fields only. Variant-specific data (price, options) is available through the special field types below (Price, Variant Field, Variant List).
* **Variant-level blocks** access variant fields and shared fields — they inherit from the parent product.
See [Product Models](/commerce-concepts/product-models#product-level-vs-variant-level-blocks) for a deeper look at when to use which level.
### Response schema
Define the output — the fields your block returns. Each field you add has a **name** that determines how it appears in the API response. The return shape depends on the field type — a storage field returns the type defined in the schema (string, number, composite, etc.), a nested block returns the nested block's full response, a listing returns a collection, and a route returns URL and metadata.
#### Storage fields
Select a field from the connected storage. The return type matches the storage field's type — a string field returns a string, a composite like `Price` returns the full composite structure, an array field returns an array.
* **Reference** — the storage field to read from
#### Nested block
Nest another Detail Block inside this block. A typical use case: nesting a brand information block inside a product detail block.
* **Block** — the Detail Block to nest
* **Parameters** — configure the nested block's parameters so it resolves correctly
#### Nested listing
Nest a Search Listing inside this block. A typical use case: a category page block with a product listing nested inside it.
* **Listing** — the Search Listing to nest
* **Parameters** — configure the listing's parameters for resolution
#### Route
Adds a context-aware URL to the response that points to a [Page URL](/api-builder/pages). Frontic resolves this and returns the correct URL for the caller's locale and region — a product card on `demo-shop.com/de` automatically links to `/de/sneakers` and on `demo-shop.com` to `/en/sneakers`.
* **Target page** — the Page URL to link to
* **Key** — a block field to take the record key from
The following fields are available on product-level blocks from a commerce storage.
#### Variant list
Includes a list of variants in the response.
* **Variant block** — a variant-level block that shapes each variant's data
* **Active variants only** — whether to exclude inactive variants
#### Variant field
Rolls up a single field across all variants of the product — for example, all available colors or sizes.
* **Storage field** — which field to aggregate from the variants
* **Active variants only** — whether to exclude inactive variants
* **Unique values only** — whether to deduplicate the aggregated values
#### Price
Includes the price in the response, resolved to the caller's currency via [Request Context](/api-builder/overview#request-context).
* **Strategy** — which price to return when a product has multiple variants: `first`, `lowest`, or `highest`
* **Active variants only** — whether to exclude inactive variants from the price calculation
## Fetch a block
```ts theme={"theme":"css-variables"}
const block = await client.block("ProductCard", "my-product-key");
```
```vue theme={"theme":"css-variables"}
```
Every block requires a `key` parameter that identifies the record. If the block is connected to a storage, this is the record's `key` field. Without a storage connection, pass any string.
See the [Client SDK reference](/reference/client-sdk#client-block) for the full API.
## Response
The response is a JSON object shaped exactly like the block's response schema:
```json theme={"theme":"css-variables"}
{
"name": "Aurora Trail Runner",
"price": { "amount": 12900, "currency": "eur", "precision": 2 },
"image": { "url": "https://cdn.demo-shop.com/aurora-trail.jpg", "alt": "Aurora Trail Runner" }
}
```
## API Playground
Preview — the API Playground is in early access. It runs against your live backend today, but the UI and feature set are still settling before general availability. [Reach out](mailto:support@frontic.com) if you want to try it.
Open the playground from any block's detail page to test it against real data without writing code. Enter a record key, pick a release stage, set the request context, and hit **Send** — the response appears side by side with your request.
Use the playground to:
* **Verify the response shape** — confirm your block returns the fields you expect before wiring up the frontend
* **Pick a release stage** — run the request against `develop`, `preview`, or `public`. Preview and public only appear when those environments exist on the project
* **Test different contexts** — pick **Domain** to send a `contextDomain` and let Frontic resolve the matching context, or **Key** to send an explicit `contextKey`
* **Pick records from storage** — open a record browser scoped to the block's connected storage and click a row to fill the key, instead of copy-pasting it from elsewhere
* **Debug unexpected data** — check response status, timing, and payload size at a glance
* **Copy code snippets** — the playground generates ready-to-use code from your current configuration, so you can paste it straight into your project
# Search Listings
Source: https://docs.frontic.com/api-builder/listings
A Search Listing returns collections of items — product grids, article archives, search results — with filtering, sorting, full-text search, pagination, and facets built in. Each item in the collection is shaped like a [Detail Block](/api-builder/blocks).
Manage your listings in the [API Builder](https://app.frontic.com/builder/listings) section of the admin app.
## Create a listing
Head to the [API Builder](https://app.frontic.com/builder/listings) and click ** Add Listing**.
Frontic generates an endpoint URL as you type the name.
Listing names must use normal capitalized words with spaces (e.g., `Product Listing`, not `ProductListing`). Frontic derives the namespace and endpoint slug from this value — incorrect casing will break the auto-generated endpoint.
After the listing is created, configure it in the **Settings** and **Query** tabs — pick the [Detail Block](/api-builder/blocks) that shapes each item, define input parameters, and set up filters, sort options, full-text search, and pagination.
## Listing Settings
The **Settings** tab covers what the listing accepts as input, how results are returned, and which storage it reads from.
### Input Parameters
Parameters let you scope a listing to a specific subset — for example, fetching products in a specific category. Parameters can be strings (a category ID) or arrays (a list of product IDs).
In the **Base Query** card inside the **Query** tab, map parameters to query criteria that filter the results.
Parameters are typically controlled by the frontend based on context (the current category, the current brand), while query options (search, filters, sorting, pagination) are controlled by the user. This separation keeps the core content gated while letting users refine results.
### Result Strategy
When the connected storage is a commerce storage, choose which level the listing returns:
* **Product level** — one row per product. Returns shared fields (name, description, brand, categories). Variant-specific data is derived via the connected block's Price, Variant Field, and Variant List fields. Use for category grids, search results, and product cards.
* **Variant level** — one row per variant. Returns variant fields (price, options, SKU, stock) plus the inherited shared fields. Use for cart line items, wishlist rows, and variant pickers.
See [Product Models](/commerce-concepts/product-models#product-level-vs-variant-level-blocks) for the full picture.
### Storage
The listing reads from a connected [Data Storage](/data-integration/data-storages). The Result Strategy above selects which level of that storage drives the rows.
## Query Settings
The **Query** tab controls how the listing can be filtered, searched, sorted, and paginated.
### Base query
A pre-defined query that always applies to the listing — every request runs through these conditions before user filters are applied. Use it to scope the listing to a subset of records (e.g. only active products, only items in a specific category).
Each condition has three parts:
* **Field** — a storage field or [block field](/api-builder/blocks) to match against. The value input adapts to the field's type, so booleans, numbers, strings, and enums all get the right widget.
* **Operator** — how to compare. Available operators:
* `equals` / `notEquals` — exact match. Works on both string and array fields — on arrays, Frontic turns it into an "includes" check under the hood. This is the operator to use when matching against a field that holds a list of values (e.g. category IDs).
* `like` / `notLike` — partial match (contains)
* `gt` / `gte` / `lt` / `lte` — numeric comparisons
* **Value** — a static value or a [mapped parameter](#input-parameters), set via the source toggle on the right of the input:
* **Static** — compare against a fixed value. The widget matches the field type: a switch for booleans, a number input for integers and floats, a dropdown for enums, a text input for everything else.
* **Mapped** — compare against an input parameter. The picker only lists parameters whose type matches the field. If exactly one matches, it's selected automatically. If none match, the picker is disabled with a "No matching parameter" hint.
Changing the field afterwards clears the value and resets the operator, so a condition can't end up with a value type that doesn't match the field.
You can add multiple conditions and choose whether records must match **all** of them or **any** of them.
### Filter options
Filter, search, sorting, and pagination settings are configured directly in the **Query** tab below the base query.
Select which fields users can filter on. Each selected field becomes a filter in the API response with facet data your frontend can render as checkboxes, dropdowns, or sliders.
* **Fields** — select from block fields or storage fields. Each selected field becomes a filterable dimension.
* **Facets** — enabled by default (up to 200 values per filter). Disable for high-cardinality fields like price ranges to reduce response size. See [Filter facets](#filter-facets) below for the response shape.
* **Active vs. total** — facets are returned in two sets: `result` (matching the current filters) and `total` (all items). Your UI can show both "X results with this filter" and "Y total items."
#### Dynamic filters
When you select an [`Option` composite](/data-integration/data-storages#option-fields) as a filter, Frontic auto-generates **dynamic facets grouped by attribute**. One Option field produces separate filter groups for color, size, material, etc. — no manual setup needed. The facets are aggregated across variants, so a product-level listing shows all available options.
### Searchable fields
Select which fields support full-text search. Often used for product names, aliases, or descriptions.
* **Fields** — select from block fields or storage fields. Multiple fields can be searchable at once.
* **Fuzzy matching** — enabled by default to counteract typos. Disable for exact matches only.
### Sorting
Define which fields users can sort by.
* **Sort fields** — select from block fields or storage fields
* **Default sort** — which field and direction (ascending/descending) to use when no sort is specified
* **Multiple options** — users can choose between up to 3 sort options
#### Dynamic sortings
For category-specific product positioning or other parameterized sort orders, use the built-in "Sorting" data type in your storage. When you add it as a sort option, Frontic asks you to select a parameter whose value matches the key inside your sorting structure. Missing keys are handled gracefully.
Sorting keys (and the parameter) must be strings.
### Pagination
* **Default page size** — how many items per page when no limit is specified
* **Maximum page size** — the upper bound for the `limit` parameter
## Fetch a listing
The minimal call takes the listing name and its parameters:
```ts theme={"theme":"css-variables"}
const { items } = await client.listing("ProductListing", {
categoryId: "shoes",
});
```
```vue theme={"theme":"css-variables"}
```
Add a `query` object to filter, sort, search, and paginate. Filters support `equals`, `range`, `contains`, and `and` / `or` / `not` for chaining — by default they're AND-combined. Passing an array of values to `equals` or `contains` is treated as an OR group.
```ts theme={"theme":"css-variables"}
const { items } = await client.listing(
"ProductListing",
{ categoryId: "shoes" },
{
query: {
filter: [
{ field: "price.amount", type: "range", from: 3000, to: 10000 },
{
type: "or",
filter: [
{ field: "properties.activity", type: "equals", value: "Running" },
{ field: "properties.activity", type: "equals", value: "Cycling" },
],
},
],
sort: { field: "publishedAt", order: "desc" },
search: "trail",
page: 1,
limit: 24,
},
},
);
```
```vue theme={"theme":"css-variables"}
```
For interactive search UIs, [`useFronticSearch`](/reference/nuxt-module#usefronticsearch) wraps `useFronticListing` with reactive state and ready-to-use methods for adding/removing filters, sorting, paginating, searching, and resetting state.
See the [Client SDK reference](/reference/client-sdk#client-listing) and [Nuxt module reference](/reference/nuxt-module#usefronticlisting) for the full API surface, including all filter operators and per-request overrides.
## Response
The response contains items and metadata for building your UI.
```json theme={"theme":"css-variables"}
{
"items": [],
"total": 998,
"page": {
"last": 42,
"limit": 24,
"current": 1,
"next": null,
"prev": null
},
"filter": {},
"aggregation": {},
"sort": []
}
```
The resulting items, each shaped like the connected Detail Block.
Total number of items matching the query.
Current page information: `current`, `last`, `limit`, `next`, `prev`.
Facet data for each configured filter field.
Aggregation results (min, max, avg, sum, total) for configured filter fields.
Configured sorting options (up to 3).
### Filter facets
Each entry under `filter` describes one filter's available options under the current query. Every option carries `option`, `value`, `count`, `selected`, and `disabled` — enough for your UI to render checkboxes, dropdowns, or sliders without extra requests.
```json theme={"theme":"css-variables"}
{
"filter": {
"activity": [
{ "option": "Running", "value": "Running", "selected": true, "count": 42, "disabled": false },
{ "option": "Cycling", "value": "Cycling", "selected": false, "count": null, "disabled": false }
]
}
}
```
By default, filters are AND-combined and `count` / `disabled` reflect that assumption. When a filter is applied as an OR group, only selected options get a real count — non-selected options return `count: null` so your UI can keep them selectable without showing a misleading number.
### Aggregations
For each filter field, `aggregation` returns `result` (matching the current query) and `total` (across all items). Numeric fields include `min`, `max`, `avg`, `sum`, `total`; string fields include only `total`. Disable aggregations on individual filter fields to reduce response size.
## API Playground
Preview — the API Playground is in early access. It runs against your live backend today, but the UI and feature set are still settling before general availability. [Reach out](mailto:support@frontic.com) if you want to try it.
Open the playground from any listing's detail page to test queries against real data without writing code. The listing playground gives you a full visual interface for the query layer.
**What you can do:**
* **Fill parameters** — each listing parameter gets its own typed input field, pre-filled with defaults where available
* **Pick a release stage** — run the request against `develop`, `preview`, or `public`. Preview and public only appear when those environments exist on the project
* **Paginate** — page and limit steppers that sync with the response. If you exceed the last page, the playground auto-clamps
* **Search** — type a search query and see how your searchable fields respond
* **Sort** — pick a sort field and direction from the configured options
* **Build filters visually** — a filter builder that supports AND/OR groups matching your configured filter options. After the first request, facet values from the response are loaded into the filter inputs so you can pick from real data
* **Switch context** — pick **Domain** to send a `contextDomain` and let Frontic resolve the matching context, or **Key** to send an explicit `contextKey`
* **Inspect the response** — status, response time, and payload size are shown alongside the body so you can spot performance issues. The full JSON response is browseable and copyable
* **Copy code snippets** — auto-generated from your current configuration, ready to paste into your project
* **Edit raw JSON** — switch to the raw tab to craft custom request payloads by hand
# Overview
Source: https://docs.frontic.com/api-builder/overview
The API Builder turns your [Data Storages](/data-integration/data-storages) into the **exact API surface** each part of your experience needs.
One block per component, one listing per collection, one page per route, one tree per hierarchy — nothing more, nothing less. Your frontend asks for what it needs, and the backend returns exactly that, already scoped to the caller's request context.
[Buddy](/essentials/buddy) can create and configure Detail Blocks, Search Listings, and Page URLs for you. Describe what your experience needs and Buddy handles the setup.
## The primitives
*What fields does this component need?* An API endpoint that returns
the exact data one UI component needs — reusable, parameterized, nestable.
*How do I query a collection?* Filtering, sorting, full-text search,
pagination, and facets — each item shaped like a block.
*What renders at this URL?* Ties a URL pattern to a Detail Block so
your router resolves content in a single request.
*What's the hierarchy?* Returns records as a tree — navigation menus,
category hierarchies, anything with a parent/child structure.
## How they compose
A **Detail Block** is an API endpoint shaped like a component. You
define a `ProductCard` block with a schema that returns exactly the
fields that component renders — image, title, price, badges, CTA
text. The block connects to a Data Storage and describes which
fields map into its response shape.
A **Search Listing** returns a collection of items, each shaped like
a block. A `ProductListing` listing takes a `category` filter
parameter and returns a list of `ProductCard`-shaped items, with
facets and pagination attached. Listings handle the query layer —
filter, sort, search, paginate — and delegate the per-item shape to
the block from the previous step.
A **Page URL** ties a URL pattern to a single Detail Block. A
`Product Detail` page binds `/products/{slug}` to the `ProductCard`
block — each URL resolves to exactly one record, and the same block
schema your grid uses also drives the detail view. The response
includes the page type and the matching data, so your frontend
router can pick the right component and render it straight away.
The block itself can nest a `ProductListing` (e.g. related products)
when the page needs collections.
## Request Context
Every Detail Block, Search Listing, and Page URL response is shaped by the caller's **request context**. That means one block can serve many markets, many audiences, and many languages from a single definition.
Request context is a combination of three values that together tell the backend *who's asking*:
The **audience variant** — B2B vs. B2C, different brands, wholesale vs. retail.
The **geography** — which locales are supported, which currency applies.
The **language** (`en`, `de`, `fr`) — translatable fields return the matching variant.
### Where context comes from
Your project maps domains (or paths) to `scope + region + locale` combinations in [Project Settings → Domains](/project-settings/domains). Pass the domain as `contextDomain` and Frontic resolves the matching context automatically.
```ts theme={"theme":"css-variables"}
const data = await client.block('CategoryDetail', 'running-shoes', {
contextDomain: 'www.demo-shop.com/de',
});
// Resolves → German region, de locale, public scope
```
```vue theme={"theme":"css-variables"}
```
A typical domain mapping:
| Domain | Scope | Region | Locale |
| ---------------------- | -------- | ------------------- | ------ |
| `www.demo-shop.com` | `public` | United States (USD) | `en` |
| `www.demo-shop.com/de` | `public` | Germany (EUR) | `de` |
| `b2b.demo-shop.com` | `b2b` | United States (USD) | `en` |
### Why this matters
* A `ProductListing` listing in the German domain returns the German product catalog in EUR with German copy — without market-specific code.
* A `ProductCard` block in a B2B scope returns B2B-specific images, descriptions, and pricing for the same product — without a separate B2B block.
* A Page at `/products/:slug` resolves to different URLs and different records in every domain — without duplicate route definitions.
You write each primitive once. The backend delivers the right variant based on who's asking.
### Handle context keys
Every response includes a `contextKey` in the `fs-context` header. This key tracks the session's context settings — scope, region, locale — and lets overrides survive across requests.
* **No key sent** → Frontic generates a new one with the settings from the resolved domain and returns it in the response
* **Your own key sent** (any string, 36–50 characters) → Frontic persists it with the current context settings if it doesn't exist yet, or loads the stored settings if it does
* **Key sent on subsequent requests** → Frontic applies the stored settings, even if they differ from the domain default
Store the key from the first response and pass it on every subsequent request. When a visitor switches language or currency, update the key's settings — all following requests reflect the change without touching the domain mapping.
#### Updating an existing contextKey
To change a visitor's active locale or region without minting a new key, update the stored settings on the current `contextKey` — the [Client SDK](/reference/client-sdk#clientcontextupdate) and the [Nuxt module](/reference/nuxt-module#usefronticcontext) both expose methods for this so you don't hand-craft the HTTP call. Every subsequent request using the same key reflects the change.
```ts theme={"theme":"css-variables"}
await client.contextUpdate(
{ region: 'eu', locale: 'de-DE' },
contextKey,
);
```
```ts theme={"theme":"css-variables"}
const { update } = useFronticContext();
await update({ region: 'eu', locale: 'de-DE' });
```
Common triggers:
* A visitor opens a language selector and switches from `en` to `de` — update the key with the new locale.
* A visitor picks a different market from a region dropdown (United States → Rest of Europe) — update the key with the new region; currency follows the region's setting.
* A visitor updates their preferences on a profile screen — update whichever settings changed.
### Missing or ambiguous context
If a request has no `contextDomain` or the domain doesn't match any configured mapping, Frontic falls back to the project's default region and locale. A missing `contextKey` is never an error — Frontic creates a new one automatically. An unknown key is treated the same way: persisted as a fresh context with the current domain settings. The only invalid key is one that doesn't meet the format requirements (36–50 characters).
## API Playground Preview — the API Playground is in early access. It runs against your live backend today, but the UI and feature set are still settling before general availability. [Reach out](mailto:support@frontic.com) if you want to try it.
Every Detail Block, Search Listing, Menu Tree, and Page URL has a built-in **API Playground** accessible from its detail page. It lets you send real requests against your backend, inspect responses, and experiment with stage, context, filters, and parameters — without writing code or leaving the admin app.
The playground shows the full endpoint URL, picks a release stage (`develop`, `preview`, or `public`) to target, lets you configure headers (`fs-version`, `fs-secret`, `fs-domain`, `fs-context`), and generates ready-to-use code snippets from your current configuration. A resource sheet in the dialog header lets you swap between primitives — pages, blocks, listings, and trees — without closing and reopening. See [Detail Blocks](/api-builder/blocks#api-playground), [Search Listings](/api-builder/listings#api-playground), [Menu Trees](/api-builder/trees#api-playground), and [Page URLs](/api-builder/pages#api-playground) for the full details.
## Shipped through Releases
Changes to blocks, listings, and pages flow through [Releases](/releases/overview) alongside Data Storage schema changes, Data Sync changes, and project settings. You don't ship API changes piecemeal — you bundle everything that changed into a release and roll it out together, with preview and rollback.
## Where to go next
Schema, parameters, storage connection, nesting — everything blocks can do.
Filters, sorts, search, facets, and pagination.
Routing, URL patterns, localization, error handling.
# Page URLs
Source: https://docs.frontic.com/api-builder/pages
Shopping experiences live on URLs. Customers bookmark them, search engines index them, social posts link to them. Every one of those URLs needs to resolve to something renderable — and it should do so in a single request.
That's what a Page URL is: you hand Frontic a URL-key, and you get back everything needed to render the page — the page type, the data, and all the route metadata. No server-side routing logic, no second lookup, no stitching.
Manage your pages in the [API Builder](https://app.frontic.com/builder/pages) section of the admin app.
Page URLs don't hold their own data — they map a URL pattern to a [Detail Block](/api-builder/blocks), and that block's records become the page's content. Make sure the block you want to serve already exists and is connected to a Data Storage before creating a page.
## Create a page
From the [API Builder](https://app.frontic.com/builder/pages), click **New Page**.
Three pieces of information define a page:
* **Page name** — a human-readable name (e.g. `Product Detail`). Frontic derives the page type identifier and endpoint slug from this, so use normal capitalized words with spaces.
* **Block** — the [Detail Block](/api-builder/blocks) that shapes the response for this page type
* **Slug** — the block field used to generate the URL (e.g. `name`, `breadcrumbs`, `handle`)
If no field on your block produces the URL shape you want, add a dedicated slug field and compute it with the [Value Composer](/data-integration/value-composer). Common patterns: `slugify` on a product name, `concat` a category path with a product name and color variant, or combine a brand prefix with a SKU — full control over URL shape without reshaping the underlying data.
### Localized URLs
If the slug field points at a translatable or scoped storage field, Frontic folds out one URL per locale and region from the same record. A single product can resolve at `/en/shop/equipment`, `/de/shop/ausstattung`, and a B2B-scoped `/pro/shop/equipment` at the same time — each served from the [domain](/project-settings/domains) that matches, each resolving to its own [request context](/api-builder/overview#request-context). Translate or scope the slug field on your Data Storage and the URL structure follows.
### Action on withdrawal
Pages change — blocks get removed, records get deactivated. In commerce, URLs that disappear cost you rankings, inbound links, and customer trust. Configure what happens when a page's underlying record is no longer available:
| Action | Description |
| -------------------- | ----------------------------------------------------- |
| 302 Redirect to home | Temporary redirect — the most common choice |
| 301 Redirect to home | Permanent redirect, removes the URL from search index |
| Delete URL | Removes the URL entirely (callers get a 404) |
| Keep URL with 404 | Keeps the URL but serves a 404 — useful for analytics |
Withdrawn URLs aren't lost — they're detached from the record but kept in the URL history. If a record with the same slug reappears later, Frontic automatically re-attaches the existing URL to it, preserving the original link. A record recreated with a different slug gets a fresh URL, and the old one stays withdrawn.
To inspect individual generated URLs, verify canonical status, or remove stale entries, use the [URL Browser](/commerce-concepts/urls-seo#url-browser) in the admin app.
### Conflict resolution
URLs must be unique. When two records would generate the same slug (e.g. two products with the same name), pick a strategy:
| Strategy | Description |
| ------------ | ---------------------------------------------------------------------- |
| Append token | A secondary field (e.g. SKU) is appended to the slug to make it unique |
| Dismiss | The second page isn't created — the colliding record gets no URL |
### Sitemaps
You can augment your sitemaps with your Frontic page URLs. Enable sitemap visibility in your page settings to include that page in a generated sitemap:
Sitemaps can be fetched via [the Fetch API's sitemap endpoint](/reference/fetch-api/sitemap).
## How URLs resolve
Frontic stores pages by their content-only slug, then projects them through your project's domain mappings to produce one fully-qualified URL per `(scope, region, locale)` combination that's configured to expose the content.
### URL anatomy
A resolvable URL splits into two parts: the **domain** the project mapped, and the **slug** the page record produced.
```
www.demo-shop.com/de /shop/ausstattung
└──────────────────┘ └───────────────┘
domain slug
```
The **domain** is whatever string the project configured under [Domains](/project-settings/domains) — Frontic doesn't model a separate locale prefix layer. The same project can map any of these as a domain entry, and Frontic treats the whole thing as opaque on lookup:
* `www.demo-shop.com` — single domain, single locale
* `de.demo-shop.com` — locale on a subdomain
* `www.demo-shop.com/de` — locale on a path prefix, on the same domain
Each domain entry pins its own scope, region, and locale defaults. The slug is whatever path the page record's slug field produced — translated independently when the field is translatable.
The same product can resolve at all of:
* `www.demo-shop.com/shop/equipment` — public scope, en-US
* `www.demo-shop.com/de/shop/ausstattung` — public scope, de-DE (translated slug, locale carried by the path prefix in the configured domain)
* `b2b.demo-shop.com/shop/equipment` — b2b scope, en-US
Each is a separate URL with its own [request context](/api-builder/overview#request-context); the response shape is identical, but the data inside reflects the resolved scope / region / locale.
### Resolution chain
When a request hits Frontic with a URL like `www.demo-shop.com/de/shop/ausstattung`, the pipeline runs in this order:
1. **Domain match** — Frontic finds the longest configured project-domain entry that owns this URL (e.g. `www.demo-shop.com/de`, not `www.demo-shop.com`). The match returns the scope, region, and locale that domain entry is pinned to.
2. **Context resolution** — the `(scope, region, locale)` tuple becomes the [request context](/api-builder/overview#request-context). If the caller sent an `fs-context` header, that takes precedence; otherwise the domain's defaults apply.
3. **Page lookup** — Frontic strips the matched domain prefix and looks the remaining slug up in the project's URL history for the resolved scope. Hits return the page record; misses fall through to `route.code: 404` (or a redirect, depending on the page's withdrawal action).
4. **Block render** — the matched page's connected [Detail Block](/api-builder/blocks) renders the response, applying the resolved context to translatable / scoped fields.
5. **Route metadata** — Frontic attaches `route` to the response with the resolved code, the active context, alternate URLs for the same content in other regions/locales, and (when relevant) `redirect` or `context.suggested` entries.
The frontend doesn't need to know the chain — it's enough to pass the incoming URL to `client.page()`. The pipeline produces a deterministic answer, and your match-all route renders whatever comes back via `page.type` and `page.data`.
## Fetch a page
Your frontend hits a match-all route with only the incoming slug (e.g. `/fr/femme/chemises`). Pass that slug to Frontic and render the result.
```ts theme={"theme":"css-variables"}
const { slug } = useRoute().params;
const page = await client.page(slug);
```
```vue theme={"theme":"css-variables"}
```
See the [Client SDK reference](/reference/client-sdk#client-page) for the full API.
## Response
The response carries the page type, the block payload for this URL, and route metadata.
```json theme={"theme":"css-variables"}
{
"type": "CategoryDetail",
"block": "CategoryPage",
"data": {
"slug": "gloves-0194bcdaa2d57dc4bb4a9f33ed6b718c",
"title": "Gloves"
},
"route": {
"code": 200,
"context": { "region": "de", "locale": "de-DE" },
"alternates": [
{
"slug": "shop/equipment",
"path": "/en/shop/equipment",
"url": "www.demo-shop.com/en/shop/equipment",
"href": "https://www.demo-shop.com/en/shop/equipment",
"region": "en",
"locale": "en-GB"
}
]
}
}
```
The page type (e.g. `"CategoryDetail"`, `"ProductDetail"`). Use this to pick which component to render.
The underlying Detail Block name. Useful if you map components to block names instead of page types.
The block's content for this URL. Structure depends on the Detail Block configuration.
Route metadata — response code, resolved context, alternates, suggestions, redirects. See [Route metadata](#route-metadata) below.
### Route metadata
The `route` property tells your frontend what HTTP code to return, what locale/region was resolved, where to redirect, and which alternate URLs exist for the same content.
The HTTP status code your frontend should return: `200`, `301`, `302`, or `404`.
The resolved region and locale the page was served for. Use this for analytics, canonical tags, or client-side state.
Alternate routes for the same content in other regions/locales. Use for `hreflang` tags and language switchers. The current route is not included.
When `code` is `301` or `302`, this holds the target URL to redirect to.
When the caller's [context](/api-builder/overview#request-context) doesn't match the route Frontic resolved, this suggests a better-matching alternate. See [Context mismatches](#context-mismatches).
### Context mismatches
When a visitor with a `contextKey` tied to one region or locale lands on a URL from a different one — say, a user with `de-DE` context hitting `/en/shop/equipment` — Frontic serves the requested URL (never guesses silently) but adds `context.suggested` to the route, containing the matching alternate (slug, path, url, href, region, locale).
Your frontend decides what to do: prompt the user ("we noticed you're in German — switch?"), auto-redirect, adjust the canonical tag, or ignore the suggestion entirely and render the content as-is. Suggestions only appear on `200` responses — redirects and 404s don't include them.
### Error handling and redirects
The HTTP call itself returns `200 OK` whenever Frontic can satisfy the request — logical redirects and not-found cases for the URL live inside `route.code`. Your frontend reads that field, returns the matching HTTP status to the visitor, and (for `301` / `302`) uses `route.redirect.href` as the location. One code path handles every case.
## API Playground
Preview — the API Playground is in early access. It runs against your live backend today, but the UI and feature set are still settling before general availability. [Reach out](mailto:support@frontic.com) if you want to try it.
Open the playground from the Pages list header or from any routed page's detail header to look up a rendered page against real data without writing code. Enter a URL, pick a release stage, set the request context, and hit **Send** — the full page response appears side by side with your request.
Use the playground to:
* **Resolve a URL** — paste the full URL (e.g. `www.demo-shop.com/de/products/red-sneaker`) and see which page type, block, and data Frontic resolves for it
* **Pick a release stage** — run the request against `develop`, `preview`, or `public`. Preview and public only appear when those environments exist on the project
* **Inspect the response** — status, response time, and payload size are shown alongside the body so you can spot performance issues. The response includes `type`, `block`, `data`, and the full `route` metadata
* **Copy code snippets** — auto-generated from your current configuration, ready to paste into your project
## Link to a page from a block
Block responses can embed a URL that points to a page. Add a [Route field](/api-builder/blocks#route) to a block, pick the target page and the field that carries the record key, and Frontic resolves the matching URL for every response — in the caller's locale and region, under the right domain. A product card rendered on `demo-shop.com/de` gets `/de/turnschuhe/air-max`, the same card on `demo-shop.com/en` gets `/en/sneaker/air-max`. No URL building on the frontend, no locale mapping tables.
## Related
Shape the response for each page type.
Build hierarchical navigation from Page URLs.
How URLs are generated, how history is preserved, and how SEO metadata flows.
How scope, region, and locale shape every page response.
# Menu Trees
Source: https://docs.frontic.com/api-builder/trees
A Menu Tree returns a hierarchical collection of records — navigation menus, category trees, support article hierarchies, anything with a parent/child structure.
Manage your trees in the [API Builder](https://app.frontic.com/builder/trees) section of the admin app.
## How it works
A Menu Tree has two sides: how records connect into a hierarchy, and what each node looks like when it comes back.
### Structure
A Menu Tree reads records from a [Data Storage](/data-integration/data-storages) and assembles them into a tree using two default fields:
* **`parentKey`** — links each record to its parent node.
* **`position`** — numeric sort order among siblings. Always present on storages used by a Menu Tree.
### Node shape
One [Detail Block](/api-builder/blocks) defines the node shape for the whole tree. Every record returned — the root, every child, every grandchild, all the way down to the leaves — is rendered through that same block, so the payload stays consistent at any depth. On top of that, Frontic attaches a single tree-level property, `$items`, to each node to carry its subtree. Everything else on a node comes straight from the block you picked.
Menu Trees don't hold their own data — they read records from a [Data Storage](/data-integration/data-storages) and render each node through a [Detail Block](/api-builder/blocks). Make sure the block you want to serve already exists and is connected to the storage before creating a tree.
## Create a tree
Head to the [API Builder](https://app.frontic.com/builder/trees) and click ** Add Tree**.
Give the tree a name using capitalized words with spaces — `Shop Menu`, `Footer Navigation`, `Support Links`. Frontic derives the endpoint slug (e.g. `shop/menu`) and the SDK type name from this value, so the name is permanent. Click **Continue** to land in the tree builder, where you pick:
* **Storage** — which Data Storage the nodes come from
* **Node block** — the Detail Block that shapes each node's response
### Query
In the **Query** tab, you can configure a pre-defined query that always applies to the tree — every request runs through these conditions. Use it to scope the tree to a subset of records (e.g. only active categories, only categories with a specific attribute).
Each condition has three parts:
* **Field** — a storage field or [block field](/api-builder/blocks) to match against. The value input adapts to the field's type, so booleans, numbers, strings, and enums all get the right widget.
* **Operator** — how to compare. Available operators:
* `equals` / `notEquals` — exact match. Works on both string and array fields — on arrays, Frontic turns it into an "includes" check under the hood. This is the operator to use when matching against a field that holds a list of values (e.g. category IDs).
* `like` / `notLike` — partial match (contains)
* `gt` / `gte` / `lt` / `lte` — numeric comparisons
* **Value** — a static value to compare against a fixed value. The widget matches the field type: a switch for booleans, a number input for integers and floats, a dropdown for enums, a text input for everything else.
Changing the field afterwards clears the value and resets the operator, so a condition can't end up with a value type that doesn't match the field.
You can add multiple conditions and choose whether records must match **all** of them or **any** of them.
## Fetch a tree
```ts theme={"theme":"css-variables"}
// Full tree
const tree = await client.tree("CategoryNavigation");
// Subtree from a specific node, 2 levels deep
const tree = await client.tree("CategoryNavigation", {
key: "shop",
depth: 2,
});
```
```vue theme={"theme":"css-variables"}
```
### Parameters
Optional starting point. When provided, `items` contains a single entry — the node with this key, plus its descendants. Without a key, `items` contains every root-level node (records with no parent).
Optional level limit. Controls how many levels of `$items` are included in the response.
## Response
```json theme={"theme":"css-variables"}
{
"items": [
{
"key": "shop",
"name": "Shop",
"link": {
"slug": "shop",
"path": "/en/shop",
"url": "www.demo-shop.com/en/shop",
"href": "https://www.demo-shop.com/en/shop"
},
"$items": [
{
"key": "women",
"name": "Women",
"link": {
"slug": "shop/women",
"path": "/en/shop/women",
"url": "www.demo-shop.com/en/shop/women",
"href": "https://www.demo-shop.com/en/shop/women"
},
"$items": [ /* ... */ ]
},
{
"key": "men",
"name": "Men",
"link": { "slug": "shop/men", "path": "/en/shop/men", "href": "https://www.demo-shop.com/en/shop/men" },
"$items": [ /* ... */ ]
}
]
}
]
}
```
The top-level `items` always contains every root-level node (records with no parent). When you call with a `key`, `items` still contains a list — but with just that single matching node — kept as an array for shape consistency.
Each node's shape (beyond `key` and `$items`) is defined by the Detail Block you selected. In this example, the block carries a [Route field](/api-builder/blocks#route) named `link` that resolves to each node's Page URL in the caller's locale and region.
Field names starting with `$` are reserved. You cannot use them in your Data Storage schemas — Frontic uses the `$` prefix for tree-level metadata like `$items`.
## API Playground
Preview — the API Playground is in early access. It runs against your live backend today, but the UI and feature set are still settling before general availability. [Reach out](mailto:support@frontic.com) if you want to try it.
Open the playground from any tree's detail page to fetch the tree against real data without writing code. Pick a starting `key`, set a `depth`, pick a release stage, set the request context, and hit **Send** — the resolved subtree appears side by side with your request.
Use the playground to:
* **Resolve a subtree** — pick a starting key and depth to scope the response to the slice you actually care about
* **Pick a release stage** — run the request against `develop`, `preview`, or `public`. Preview and public only appear when those environments exist on the project
* **Switch context** — pick **Domain** to send a `contextDomain` and let Frontic resolve the matching context, or **Key** to send an explicit `contextKey`
* **Inspect the response** — status, response time, and payload size are shown alongside the body so you can spot performance issues
* **Copy code snippets** — auto-generated from your current configuration, ready to paste into your project
## Limits
A single Menu Tree response is capped at **1,000 nodes total**. Frontic resolves the tree level by level — if an entire level doesn't fit within the remaining budget, the whole level is omitted (partial levels are never returned) and a warning is included in the response header.
For large hierarchies, pass a `key` and `depth` to fetch the subtree you actually need instead of the whole structure.
## Related
Shape each node's response.
Where tree records live, with `parentKey` and `position` as default fields.
The link target each menu node points at via the Route field.
# Multi-Channel & Multi-Region
Source: https://docs.frontic.com/commerce-concepts/multi-channel-region
Shopping experiences rarely live in one market, one language, or one audience. You sell into different regions, in different currencies, in different languages — and you probably want the same platform to serve B2B and B2C, retail and wholesale, brand A and brand B, without maintaining a parallel project for each.
Frontic is built for that from day one. This page explains how **scopes**, **regions**, **locales**, **currencies**, and **domains** compose into a multi-market stack — and how you write your blocks and storages **once** while the stack delivers the right variant to each caller.
## The four axes
**Who is asking.** B2B, B2C, wholesale, different brands, different
loyalty tiers. Scopes let you ship variations of the same content to
different audiences without branching your data model.
**Where they are.** A region is a geographic market — United States,
Germany, Europe, DACH. Each region carries its own currency and its
own set of supported locales.
**What language they speak.** Locales are language codes (`en`, `de`,
`fr-CA`). Translatable fields in your storages return the variant
matching the resolved locale.
**How they arrived.** Domains map URLs (e.g. `www.demo-shop.com/de`,
`b2b.demo-shop.com/en`) to a specific scope + region + locale
combination. The mapping is how request context gets resolved
automatically for public traffic.
These four axes work together. A caller arrives at a domain → the domain determines their scope, region, and locale → that combination shapes every block, listing, and page response they get back. You configure the axes once in [Project Settings](/project-settings/general) and never think about them again.
## Configure your axes
The big idea: **one project serves every market.** You don't spin up a separate Frontic project per country, per language, or per brand. Pair the project with the codebase that renders it — generated client, release history, and deploy pipeline all line up against that one repo — and compose markets inside the project via regions, locales, currencies, and scopes. Parallel projects for parallel markets fight the grain.
Add the regions you serve — "US", "Germany", "Europe (ex-DE)",
"UK". Each region is a geographic unit with:
* a **default locale**
* a **supported locale list**
* a **currency** the stack uses for pricing responses in that region
Add the languages you support — `en-US`, `en-GB`, `de-DE`, `fr-CA`.
Locales are global to the project; regions reference them. You can
have locales that aren't available in every region (e.g. `fr` is
available in Europe and Canada but not in the US).
Most projects start with a single default scope — `public`. Add
scopes if you need audience variants: `b2b`, `wholesale`, `staff`,
`loyalty-gold`. Scoped fields in your storages (see below) are only
returned when the caller's scope matches.
Configure the domains where your experience lives and map each one
to a scope/region/locale combination. This is how public traffic
gets its request context without any client-side logic.
See [Project Settings](/project-settings/general) for the full configuration reference.
## Storage field flags
On [Data Storage](/data-integration/data-storages) fields, three flags let you control how a field behaves across the four axes:
The field holds a **different value per locale**. A translatable
`name` field on a product storage has a German variant, an English
variant, a French variant, etc. The stack serves the right one based
on the caller's resolved locale.
The field is **only available in specific scopes**. A scoped
`wholesalePrice` field exists only in the `wholesale` scope. Callers
in the `public` scope never see it, and listings can't filter on it
from the public scope.
The field holds **multiple values**. Tag lists, multi-image galleries,
multi-category assignments. Combines with Translatable and Scoped —
you can have a translatable array field of tags that differ per
language and per scope.
Plus the product-specific **Shared Across Variants** flag (see [Product Models](/commerce-concepts/product-models#sharing-fields-across-variants)) which controls whether a field lives at the product or variant level.
You mark a field once with the combination that fits — `Translatable: true, Scoped: false, Shared Across Variants: true` for a product description that's the same for every variant but different per language — and the stack takes care of the rest.
## Example: a product across markets
Here's how a single `Products` Data Storage serves a multi-market catalogue:
| Field | Flags | How it behaves |
| ---------------- | ------------------------------------ | ------------------------------------------------------------------------------------ |
| `name` | Translatable, Shared across variants | Same across variants of one product, different per locale |
| `description` | Translatable, Shared across variants | As above |
| `price` | (variant-level, built-in) | Same record across scopes/locales, but auto-returned in the caller's region currency |
| `wholesalePrice` | Scoped to `b2b` | Returned only to B2B callers; invisible to `public` scope |
| `swatchImage` | (per-variant) | Variant-specific media, returned based on which variant is resolved |
| `seo` | Translatable, Shared across variants | SEO composite — title, description, keywords resolve per locale |
All of the logic that makes this work is in the storage schema. Your blocks and listings don't care — they reference the fields they need, and the stack returns the right values for the caller.
## Shared blocks and listings
Because [Detail Blocks](/api-builder/blocks), [Search Listings](/api-builder/listings), and [Page URLs](/api-builder/pages) all read from a context-aware Data Storage, you don't need market-specific API surfaces. A single `ProductHero` block serves every market. A single `ProductList` listing serves every market. A single `/products/:slug` page resolves to different records per domain — but you wrote the page once.
That's the payoff of commerce-native infrastructure: **the hard problems of multi-market commerce are solved in the stack, not in your code**. You spend your time on the experience, not on plumbing locale-aware data fetching into every component.
## Common patterns
One region, multiple locales. Mark all user-facing text fields
Translatable. The same product records serve every language because
the stack substitutes the localized field values at response time.
Two scopes: `public` and `b2b`. B2B-only fields (wholesale pricing,
bulk discounts, tax-exempt flags) are Scoped to `b2b`. B2B domains
map callers into the `b2b` scope. Public domains stay in `public`.
One set of blocks, one set of listings, two fully-different experiences.
One scope per brand. Each brand gets its own domain(s) mapping into
its scope. Shared primitives (product categories, shared content)
stay unscoped and are reused; brand-specific content lives in scoped
fields or scoped Data Storages.
Add the locale to the project, add it to the regions it's available
in, let translators fill in the translatable fields over time.
Blocks and listings keep working — callers in other locales aren't
affected — and the moment a translation lands, it flows through.
## Related
How products and variants compose with the scope/region/locale axes.
How domains translate into actual URLs — and how URL history survives
every release.
The full picture of how scope, region, and locale get resolved per
request.
# Product Models
Source: https://docs.frontic.com/commerce-concepts/product-models
Frontic treats **products as a first-class concept**, not as generic CRUD records that happen to have a "price" field. When you create a Data Storage with the **Products** schema type, the stack gives you a parent/child product + variant model out of the box — with built-in composites for pricing, options, media, and SEO that every commerce experience needs.
This page covers how that model is shaped, how variants relate to products, and how to work with it across [Data Storages](/data-integration/data-storages), [Detail Blocks](/api-builder/blocks), and [Search Listings](/api-builder/listings).
## Products vs. Content
When you create a [Data Storage](/data-integration/data-storages), you pick a schema type:
For sellable things. Products can have **variants**, each variant can
have its own **options** and **price**, and the stack knows how to
surface both levels at the API layer.
For everything else — articles, categories, landing pages, editorial
content. Flat records without the product/variant structure.
Pick **Products** whenever you're modeling something a customer can buy. Pick **Content** for anything else.
## Product and variant
In Frontic's model, the **variant** is the buyable thing — every SKU, every cart line, every price sits at the variant level. The **product** is a virtual grouping around one or more variants, carrying the attributes they share (name, description, brand). Every product has at least one variant; a product with a single variant (a book, a non-configurable item) is still modeled this way.
The actual thing a customer purchases — a **red medium t-shirt**, a
**size 10 sneaker in black**. Every variant carries its own `options`
(color, size, material), `price`, and stock. Every buyable item in the
catalog is a variant.
The shared wrapper around one or more variants. Attributes that stay
the same across variants — name, description, brand, category — live
here so you maintain them once instead of duplicating per variant.
Every Products storage automatically includes three variant-level fields you can't remove:
* **`options`** — the list of options that define this variant (e.g. `color: red`, `size: M`)
* **`price`** — the variant's price, typed as the built-in [Price composite](/reference/storage-field-types#composites)
* **`parentKey`** — links this variant to its parent product
Plus the `key` and `active` fields every storage has.
## Sharing fields across variants
When you add custom fields to a Products storage, you decide whether each field lives at the variant level (different per variant) or is **shared across variants** (same for every variant of the product).
A good rule of thumb:
| Lives at variant level | Shared across variants |
| -------------------------------------- | -------------------------------- |
| Price, stock, SKU, barcode | Product name, description, brand |
| Specific media (variant photos) | Category, tags |
| Variant-specific options (color, size) | SEO title, shared hero image |
| Availability, weight, dimensions | Care instructions, warranty text |
The **Shared Across Variants** flag is set per field when you configure the storage. Set it once, and the stack takes care of rolling the right values up to the product or down to the variant automatically.
## Built-in composites
Frontic ships with composite field types designed for commerce. You use these the same way you use primitives — add them to your storage schema, map source data into them via the [Value Composer](/data-integration/value-composer), and they become first-class parts of your API.
Prices with currency information. Automatically filtered by the caller's
region currency via [Request Context](/api-builder/overview#request-context).
Price ranges and scaled pricing — quantity discounts, tiered pricing,
before/after sale prices.
Product options (size, color, material) with values and metadata. The
stack uses these to build dynamic facets in listings automatically.
Color or material swatches with preview images and metadata — for
variant pickers and product grids.
Images and assets with captions, thumbnails, and variants. Handles the
awkward shape commerce media usually comes in.
Meta titles, descriptions, keywords — consumed by [URLs, Redirects &
SEO](/commerce-concepts/urls-seo).
Enum for `in_stock`, `out_of_stock`, `preorder`, `backorder`. Google
Merchant Center-compatible by default.
`low`, `medium`, `high` enum for simplified stock display without
exposing real inventory counts.
These aren't bolted on — they're part of the stack. Your blocks and listings can reference any of them directly, and the API Builder knows how to shape them into useful responses.
## Product-level vs. variant-level blocks
When you create a [Detail Block](/api-builder/blocks) connected to a commerce storage, you pick which level the block operates on. The key thing to understand: **shared fields are stable** (one value per product), **variant fields rotate** (one value per variant). A variant block can read the product name, but a product block can't pick one variant's price — it needs a strategy to derive it.
Returns one record per product. Has access to **shared fields** only (name, description, brand, categories). Variant-specific data is not directly available because the block wouldn't know which variant to choose from.
To surface variant data from a product block, use the special field types:
* **Price** — derive a price from the variants using a strategy (`first`, `lowest`, `highest`)
* **Variant field** — aggregate a single field across all variants (e.g. all available colors, all EANs for search indexing)
* **Variant list** — embed the full list of variants, each shaped by a variant-level block
Use product-level blocks for category tiles, search results, and product cards.
Returns one record per variant. Has access to **variant fields** (price, options, SKU, stock) *and* **shared fields** — variants inherit everything from the parent product.
Use variant-level blocks for cart line items, wishlist rows, variant pickers, and any UI that shows a specific buyable item.
Most experiences use both: a product-level block for grids and search, a variant-level block for cart and detail views. Both connect to the same storage.
## Request context on products
Every product response is shaped by [Request Context](/api-builder/overview#request-context) before it hits your frontend. For a product storage specifically, that means:
* **Prices** are returned in the caller's currency, based on the resolved region
* **Translatable fields** (name, description, marketing copy) are returned in the resolved locale
* **Scoped fields** (e.g. B2B-only descriptions, wholesale-only specs) are returned only if the caller's scope matches
You don't write any of this logic. You mark fields `Translatable` / `Scoped` / `Shared` in the storage schema, and the stack does the rest.
## Mapping from source systems
When you connect a source system via a [Connector](/connectors/overview), its product data shape almost certainly doesn't match Frontic's clean parent/variant model. Shopify thinks about products one way, commercetools another, an ERP a third way, and a PIM a fourth. A Data Sync is where those source shapes get mapped into the canonical Frontic structure — splitting flat records into parent + variants, combining variant-level fields, normalizing prices into the Price composite, and so on. The per-field reshaping lives in the [Value Composer](/data-integration/value-composer), where Computed fields chain operations to produce the exact structure each composite type expects.
Buddy is good at this step specifically. Point Buddy at a source and describe the target shape, and it'll propose a Data Sync configuration for you to review and approve.
## Related
How scopes, regions, currencies, and locales turn one product model into
a multi-market catalog.
How product pages get their URLs, how URL history is preserved, and how
SEO metadata flows from storage to response.
The full Detail Block configuration reference, including variant list
and variant field options.
Reference for all built-in composites and primitives.
# URLs, Redirects & SEO
Source: https://docs.frontic.com/commerce-concepts/urls-seo
URLs are not a detail. They're the address every search engine, every inbound link, every bookmark, every share, and every customer relies on. If a URL that worked yesterday stops working today, you don't only lose a page — you lose rankings, referrals, and customer trust.
SEO URLs in commerce are also one of those problems nobody quite agrees on. Every team has an opinion on the right shape, every project shows up with its own demands, and most stacks leave the whole question to the storefront to answer. Strip away the arguments, though, and the underlying pattern is always the same: hand over the (localized) key the visitor hit, get back everything needed to render the page.
Frontic treats URL handling as **first-class commerce infrastructure**, not a routing afterthought. You own the strategy — what URLs look like, when to redirect, how to resolve conflicts. The stack ships the mechanics — generation, history, localized alternates, response codes, SEO metadata.
## How URLs are generated
A [page](/api-builder/pages) is how Frontic assigns a URL to content. When you create a page, you pick:
* a **block** — what kind of content does this route render? (A category page, a product page, an article page.)
* a **slug field** — which field on the block's record should become the URL slug? (`name`, `breadcrumbs`, `handle`.)
Frontic generates the actual routes automatically from your data. For every record in the connected Data Storage, a URL is minted from the slug field. Ten thousand products in your storage becomes ten thousand working URLs in your project, without you touching a router config.
## Fetch a page
The frontend doesn't need a local route table. It takes the incoming URL, asks Frontic, and gets back the block, the data, and the route metadata in one call:
```ts theme={"theme":"css-variables"}
const { slug } = useRoute().params;
const { page } = await client.page(slug);
// page.type — "CategoryDetail" | "ProductDetail" | "ArticlePage" | ...
// page.block — the block name to render against
// page.data — the actual content for this URL
// page.route — alternates, suggested redirects, response code
```
The `type` field tells your frontend which component to mount; the `data` field has the content; the `route` field has everything you need for SEO, hreflang, and redirect handling.
## URL history
The hardest part of commerce URLs isn't minting them — it's **not breaking them**. Products change names. Categories get renamed. Slugs shift because someone fixed a typo. In every one of those cases, the old URL needs to either continue working or redirect to the new one, not 404.
Frontic tracks URL history per page automatically, and lets you decide what happens when a slug changes. When you create a page, you configure an **Action on withdrawal**:
The most common choice. Temporary redirect. Use when the content is
gone and you want search engines to keep revisiting.
Permanent redirect. Use when the URL is genuinely retired and should
be removed from search indexes over time.
The URL disappears from the project entirely. Callers get a 404 and
no redirect. Use sparingly.
The URL stays in Frontic's history but serves a 404 response.
Useful when you want analytics on broken inbound traffic.
When the slug on a page changes, Frontic emits the configured response automatically — you don't write redirect rules. The old URL keeps working (or fails gracefully) forever, without you maintaining a list.
## URL Browser
Use the [URL Browser](https://app.frontic.com/builder/urls) in the admin app to inspect the URLs Frontic has generated for your project. It lives in the API Builder section next to Blocks, Listings, Pages, and Trees.
The browser shows every generated Page URL in a searchable, filterable table. You can filter by URL, key, page, domain, and canonical status; sort by creation time, update time, key, or URL; and page through larger URL sets with configurable page sizes. Selecting rows lets you delete stale URLs in bulk.
Click a row to open its detail sidebar. The sidebar shows the full URL record, including `url`, `key`, `page`, `block`, `domain`, `region`, `locale`, `isCanonical`, `pageRecordDeleteStrategy`, and timestamps. `isCanonical` tells you whether the URL is the current active URL for that record; historical URLs are kept for redirects or 404 handling. `pageRecordDeleteStrategy` mirrors the page's **Action on withdrawal** setting.
Use the URL Browser when you need to debug URL generation, verify that localized URLs were created correctly, find which page a given URL resolves to, inspect canonical versus historical URLs, launch the Fetch API playground for a specific URL, or manually remove stale URLs.
## Conflict resolution
URLs must be unique. What happens when two products with the same name would generate the same slug? You configure a **conflict resolution strategy** on the page:
| Strategy | Behavior |
| ---------------- | ------------------------------------------------------------------------------------------------------ |
| **Append token** | A secondary field (e.g. SKU, ID) is appended to the slug to disambiguate it. `shirt` + `shirt-abc123`. |
| **Dismiss** | The second page isn't created. The colliding record doesn't get a URL. |
Pick whichever fits your SEO strategy. Append is the usual choice for products; dismiss is sometimes used for low-value content.
## Localized URLs
A page in a multi-locale project automatically generates a URL **per supported locale**, and the response from `client.page(slug)` includes the full alternates list in `page.route.alternates`:
```json theme={"theme":"css-variables"}
{
"route": {
"code": 200,
"alternates": [
{ "slug": "shop/ausstattung", "path": "/de/shop/ausstattung", "region": "de", "locale": "de-DE" },
{ "slug": "shop/equipment", "path": "/en/shop/equipment", "region": "en", "locale": "en-GB" }
]
}
}
```
Use `alternates` to:
* **render `hreflang` tags** so Google serves the right language to the right user
* **build intelligent language switchers** that link to the same content in another language, not only to the home page of a different locale
* **power browser-language-based suggestions** ("we noticed you're reading in English — want to switch?")
## Context mismatches
If the caller's context (from `requestUrl` or an access token) doesn't match the URL they asked for — for example, a `de-DE` token requesting an `/en/...` path — Frontic returns a `suggested` alternate instead of silently serving the wrong content:
```json theme={"theme":"css-variables"}
{
"route": {
"code": 200,
"alternates": [ /* ... */ ]
},
"context": {
"region": "en",
"locale": "en-GB",
"suggested": {
"slug": "shop/equipment",
"path": "/en/shop/ausstattung",
"href": "https://www.demo-shop.com/en/shop/equipment",
"region": "en",
"locale": "en-GB"
}
}
}
```
Your frontend can use `context.suggested` to prompt the user, auto-redirect, or adjust the canonical tag. The stack never guesses on your behalf.
## 404s and redirects
When a URL doesn't resolve, the response still has shape — there's no special error path, only a different `route.code`:
```json theme={"theme":"css-variables"}
{
"route": {
"code": 404,
"redirect": {
"slug": "",
"path": "",
"href": "https://www.demo-shop.com"
}
}
}
```
```json theme={"theme":"css-variables"}
{
"route": {
"code": 301,
"redirect": {
"slug": "shop/equipment",
"path": "/en/shop/equipment",
"href": "https://www.demo-shop.com/en/shop/equipment"
}
}
}
```
```json theme={"theme":"css-variables"}
{
"route": {
"code": 302,
"redirect": {
"slug": "",
"path": "",
"href": "https://www.demo-shop.com"
}
}
}
```
Your frontend uses `route.code` to set its HTTP response code and `route.redirect.href` as the location. One code path, all the cases.
## SEO metadata
Frontic ships with a built-in **`SEO` composite** field type. Add it to your Data Storage schema and you get a structured field for meta title, meta description, keywords, and canonical settings, ready to flow through your blocks into the response.
```
Data Storage field: seo → type: SEO → translatable: true
```
The `SEO` composite is commerce-aware — it knows about the shapes and constraints of product SEO, category SEO, and content SEO. See [Storage Field Types](/reference/storage-field-types#composites) for the full schema.
Because the field is translatable, each locale gets its own SEO values. Because it flows through your blocks, your `CategoryDetail` or `ProductPage` block decides which fields of the `SEO` composite to expose in its response — and your frontend renders them into `
` tags per request.
## Related
The full Page URL configuration — slug fields, withdrawal actions,
conflict strategies, menus.
How scopes, regions, and locales shape URL alternates.
The `SEO` composite and every other built-in field type.
# Akeneo
Source: https://docs.frontic.com/connectors/akeneo
The Akeneo connector syncs your PIM's product catalog and category tree into Frontic. Akeneo's product models become parent products, individual products become variants, and the connector pre-resolves attribute values, currencies, and locales — so your Data Sync receives a normalized payload instead of raw PIM structures.
Auth
Token
Update methods
Trigger · Polling
Resources
Products, Categories
## What you get
Akeneo product models as parent records, individual products as variants — with attributes, families, and prices pre-resolved.
Power category pages, attach category context to other records, drive navigation.
The Akeneo connector follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers the Akeneo-specific configuration and data shape.
## Connection settings
Your Akeneo instance URL, e.g. `https://my-pim.cloud.akeneo.com`.
In your Akeneo interface, navigate to **Connect → Connection Settings** and use the **Create** button to add a new API connection.
Set up the connection like this (especially choosing **Data destination** as the **Flow type**):
Copy the Connection credentials on the page, you will need them to setup the Frontic integration.
Afterwards, make sure to configure the Akeneo Integration with the role of **API Destination**.
Use this to connect your Akeneo instance via a Connection configuration in Akeneo. Use this if you don't (or can't) install the Frontic Akeneo App.
The Akeneo Connection's Client ID given in the Akeneo configuration.
The Akeneo Connection's Client Secret given in the Akeneo configuration.
The Akeneo Connection's username given in the Akeneo configuration.
The Akeneo Connection's password given in the Akeneo configuration.
The Akeneo App for Frontic is not yet available
Use this to use your Akeneo instance's installation of the Frontic Akeneo App.
An Akeneo API connection token. Given after installing the Frontic Akeneo App from the Akeneo App Marketplace — Frontic uses this to authenticate against the Akeneo REST API.
**Akeneo Enterprise app permissions and locale alignment.** On Akeneo Enterprise instances, the connection's app permissions limit which locales it's allowed to read. The locale dropdown in Frontic shows *all* of Akeneo's enabled locales — not the subset your connection is authorised for. If you pick a locale the connection can't read, the API rejects the request at sync time and the feed errors mid-run.
Before saving the channel, open **Connect → Connection settings → Permissions** in Akeneo and confirm the connection has at least the same locales selected as the channel will pull. The two lists should match 1:1 — match what's in Frontic to what the connection is allowed to read, not the other way around.
The connection populates the channel's currency dropdown from Akeneo's enabled currencies (with their `enabled` flag respected).
## Channels
An Akeneo channel in Frontic carries the locales (Akeneo "locale codes") and currencies the connector pulls per record.
A label for the channel in Frontic.
Akeneo locale codes the connector pulls per record.
The translation used when a value is missing in another locale.
Currencies (from Akeneo's enabled set) the connector pulls. Product prices are pre-filtered to these.
The **Refresh** button re-fetches Akeneo's enabled currencies and locales — use it after enabling new ones in Akeneo.
## Data Feeds
The Akeneo connector exposes two feed types:
| Feed | What it pulls |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Products** | Akeneo product models (as parents) and individual products (as variants), with their families, attributes, prices, and translations resolved. |
| **Categories** | The full category tree, parent links preserved. |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For Akeneo specifically:
* **Updates step** — Polling is supported. The Ingest API and Triggers are not used.
* **Manual refresh** — every Akeneo feed exposes a Refresh action that re-runs the feed against the source.
### Product Feed
For a product feed, you can optionally set an attribute to be used for your products' prices.
The attribute which contains the product sale price
## What the data looks like
### Identifiers
Akeneo's `identifier` field is what Frontic uses as the record key on both sides of the parent/variant divide — for product models, it's the model `code`; for individual products, it's the product `identifier` (typically the SKU). Treat the SKU as the stable lookup key when configuring blocks, listings, and slugs that point at variant records.
### Variants
Akeneo's product models become parent records; the individual products underneath become variants linked via `parentId`. **Standalone products (no model)** are emitted twice on purpose — once as a parent and once as a self-referencing variant — so a product with no variants still has a record on each side of the parent/variant divide. Same convention as [Shopware](/connectors/shopware#variants) and the [Custom integration](/connectors/custom#product-variants).
### Currency-resolved prices
Product prices are pre-filtered to the currencies declared on the channel. Map prices into the [Price composite](/reference/storage-field-types#composites) without per-region picking logic.
### Attributes split into options and properties
Akeneo attributes don't all play the same role: some define variants (size, colour, material — the things customers pick from), others describe the product (ingredients, dimensions, care instructions). The connector's attribute extractor splits them apart and stamps each list onto the feed record under the reserved `$`-prefix namespace:
* **`$options`** — variant-defining attributes, normalized into a single list per record
* **`$properties`** — descriptive attributes that don't drive variant selection
Map either list straight into your storage's [Option](/reference/storage-field-types#composites) and property fields in the Value Composer, instead of re-deriving them from Akeneo's raw `values` structures.
### Family and attribute caching
Akeneo families and attributes are cached on the integration's Instance to avoid refetching them per record — speeds up large catalog syncs.
## Good to know
* **Read-only.** The Akeneo connector pulls from Akeneo; it doesn't push back. Catalog edits happen in Akeneo.
* **Reserved `$`-prefix keys.** The connector emits `$options` and `$properties` on every product record. Don't use `$`-prefixed keys for your own custom attributes — Frontic reserves that namespace for connector-side normalization.
## Related
How Akeneo's product models and variants compose at the API layer.
commercetools' connector — same `$options` / `$properties` split.
# commercetools
Source: https://docs.frontic.com/connectors/commercetools
The commercetools connector syncs your products and categories from a commercetools project into Frontic. Master and variant SKUs are denormalized into separate records with parent links preserved, and prices are pre-filtered to the currencies on each channel — so your Data Sync receives clean, currency-resolved payloads.
Auth
OAuth 2.0 (client credentials)
Update methods
Polling
Resources
Products, Categories
## What you get
Master products with variants split into separate records by SKU, parent links preserved, prices pre-filtered per currency.
Power category pages, attach category context to other records, drive navigation.
The commercetools connector follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers the commercetools-specific configuration and data shape.
## Set up the API client
Before adding the integration in Frontic, create a dedicated API client in commercetools Merchant Center:
In Merchant Center, go to **Settings → Developer settings → API clients** and click **Create new API client**.
Under **Scopes**, select **View** for these resources — Frontic only reads from commercetools, so view scopes are enough:
* View project settings
* View stores
* View tax categories
* View products
* View published products
* View attribute groups
* View product selections
* View categories
* View standalone prices
* View types
* View key-value documents
The connector uses **Manage** scopes for none of these. Don't grant write scopes the integration won't use.
After creating, commercetools shows the credentials **once**. Copy the **project key**, **client ID**, **client secret**, **API URL**, and **auth URL** — you'll paste them all into the Frontic connection form. If you lose the secret you'll need to create a new client.
## Connection settings
Your commercetools project key. Visible at the top of Merchant Center and on the API client credentials screen.
The API client's ID created above.
The client secret paired with the Client ID.
The commercetools API endpoint for your project's region (e.g. `https://api.europe-west1.gcp.commercetools.com`).
The OAuth token endpoint for your project's region (e.g. `https://auth.europe-west1.gcp.commercetools.com`).
The connection populates the channel's currency dropdown from your commercetools project's enabled currencies.
## Channels
A commercetools channel in Frontic carries the locales and currencies the connector pulls per record.
A label for the channel in Frontic.
commercetools locale codes the connector pulls per record.
The translation used when a value is missing in another locale.
Currencies (from your commercetools project's enabled set) the connector pulls. Product prices are pre-filtered to these.
The **Refresh** button re-fetches commercetools currencies and locales — use it after enabling new ones in commercetools.
## Data Feeds
The commercetools connector exposes two feed types:
| Feed | What it pulls |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Products** | Master products as parents and variants as separate records (one per SKU), with prices and translations resolved per channel. |
| **Categories** | The full category tree, parent links preserved. |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For commercetools specifically:
* **Updates step** — Polling is supported.
* **Manual refresh** — every commercetools feed exposes a Refresh action that re-runs the feed against the source.
## What the data looks like
### Variants
A commercetools product with multiple variants becomes a parent record + one record per variant SKU, linked via `parentId`. A simple product (single variant) is emitted as a self-parented record. See [Product Models](/commerce-concepts/product-models) for how this shape composes at the API layer.
### Currency-resolved prices
Product prices are pre-filtered to the currencies declared on the channel. Variants inherit the per-currency price layout, so map them into the [Price composite](/reference/storage-field-types#composites) without per-region picking logic in your Value Composer.
### Attributes split into options and properties
commercetools attributes arrive as `{ name, value }` pairs in the source payload. The connector's attribute extractor unwraps the envelope and splits attributes by role, stamping each list onto the feed record under the reserved `$`-prefix namespace:
* **`$options`** — variant-defining attributes
* **`$properties`** — descriptive attributes that don't drive variant selection
Map either list straight into your storage's [Option](/reference/storage-field-types#composites) and property fields in the Value Composer, instead of unwrapping commercetools' `name`/`value` structure per field.
### Product type caching
Product types are cached on the integration's Instance — large catalogs sync faster because attribute definitions aren't refetched per record.
## Good to know
* **Read-only.** The commercetools connector pulls from commercetools; it doesn't push back. Edits happen in the Merchant Center.
* **Custom attribute groups.** commercetools attribute groups come through on the raw payload. Map them in your Data Sync if you need them as first-class fields.
## What isn't supported today
Commercetools has several segmentation and pricing primitives the connector doesn't currently use. The data still comes through on the raw record where commercetools includes it, but the connector doesn't filter or resolve by these dimensions:
* **Stores.** The connector pulls products from the project root, not via the [Store-scoped endpoints](https://docs.commercetools.com/api/projects/products#in-store-endpoints). Records that exist in only some stores arrive un-filtered. Use Frontic [scopes](/project-settings/scopes) and a [base query](/api-builder/listings#base-query) on your listings to segment instead.
* **Inventory channels and distribution channels.** Channel-specific prices (`priceChannel`) and channel-specific stock (`distributionChannel`) aren't applied during ingest. The connector pulls the master prices and filters by the channel's currencies; channel-tier-specific prices show up on the raw payload but aren't unwrapped.
* **Complex pricing.** Customer-group prices, country-specific prices, and validity-windowed prices on top of currency aren't resolved. The Price composite Frontic stores reflects the master / fallback price for each currency on the channel.
If your storefront depends on any of these dimensions, [get in touch](mailto:support@frontic.com) — we're actively collecting use cases to shape native support and your scenario helps us prioritise.
## Related
How commercetools master products and variants compose at the API layer.
Akeneo's connector — same `$options` / `$properties` split.
# Contentful
Source: https://docs.frontic.com/connectors/contentful
The Contentful connector syncs entries from a Contentful space — and a specific environment — into Frontic. Each entry becomes a record on a Content feed; the connector preserves Contentful's `sys.id` as the Source ID so updates land deterministically.
Auth
Access token
Update methods
Trigger · Polling
Resources
Content entries
The Contentful connector follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers the Contentful-specific configuration.
## Connection settings
Your Contentful space ID — find it in Contentful under **Settings → General settings**.
A Content Delivery API token (read-only) for your space, with access to the environments you'll connect.
## Channels
A Contentful channel carries which translations the connector pulls per entry.
A label for the channel in Frontic.
Contentful locale codes the connector pulls per entry.
The translation used when an entry field is missing in another locale.
## Data Feeds
The Contentful connector exposes one feed type:
| Feed | What it pulls |
| ----------- | ----------------------------------------------------------------------------------------------- |
| **Content** | Every entry in the configured space and environment, with the Source ID resolved from `sys.id`. |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For Contentful specifically:
### Per-feed config
Which Contentful environment this feed reads from. Defaults to `master`. Use this to point a feed at a sandbox/preview environment without changing the connection itself.
### Update methods
* **Trigger** — wire a Contentful webhook (Settings → Webhooks) to the feed's inbound webhook URL to push changes in near-real-time.
* **Polling** — scheduled refresh.
The connector does not expose a manual feed Refresh — use Polling or trigger a webhook from Contentful to backfill.
## What the data looks like
### Source ID
Each record's Source ID is taken from Contentful's `sys.id`. References between entries (`sys.type === "Link"`) come through on the raw payload — resolve them in your Data Sync if you want denormalized fields.
### Assets
Contentful asset entries come through alongside content entries. If you want them on a dedicated storage, configure a separate feed and filter by content type at the Data Sync layer.
## Good to know
* **Read-only via the connector.** Edits happen in Contentful. The Ingest API path is for systems that supplement Contentful with extra records, not for editing entries.
* **Reference resolution lives in the Data Sync.** The connector emits entries with `Link` references intact; resolution into denormalized fields is your Value Composer's job.
## Related
Storyblok's connector — draft mode and folder filters.
For pushing supplemental records via the Ingest API.
# CSV Import
Source: https://docs.frontic.com/connectors/csv-import
The CSV Import pulls **CSV files from an SFTP server you control** and turns each row into a record on a Data Feed. Reach for it when your source system is a nightly export, a partner-supplied feed, or any process that drops a file on a server but doesn't expose an API.
Auth
SFTP credentials
Update methods
Trigger · Polling
Resources
Content (one record per row)
The CSV Import follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers the SFTP-specific configuration and the row-to-record translation.
## Install and setup
There's no plugin to install. You provide an SFTP endpoint and Frontic does the rest.
Pick a directory on an SFTP server Frontic can reach. Best practice: a dedicated user with read+delete on a single base path, not your whole filesystem.
In Frontic admin, **Integrations → Add → CSV Import**. Enter the SFTP host, port, username, password, and base path.
Frontic opens a connection, lists the base path, and confirms it reached the server. Fix credentials or firewall rules until this test passes.
Configure at least one channel (declares which project locales the rows resolve into) and one Data Feed per filename pattern you want to ingest. See the sections below.
## Connection settings
Currently the only supported transport. Reserved for future protocols.
SFTP server hostname or IP.
SFTP port.
SFTP user. Use a dedicated account scoped to the import directory.
SFTP password. Stored encrypted; redacted in logs.
The root directory the connector lists from. Feed-level configurations are evaluated relative to this path.
The settings panel shows a **Connection Status** indicator. When the SFTP connection is broken, it surfaces the reason returned by the server — so you can tell whether credentials are missing, the host is unreachable, or auth was rejected. Re-test after rotating credentials.
## Channels
Unlike connectors with source-side segmentation (Shopware sales channels, Storyblok spaces), CSV files aren't natively segmented. The CSV Import's channels exist purely to declare which **project translations** each CSV row should be resolved into. Each row gets the same payload across every translation on the channel — see "Same row across all locales" under [Good to know](#good-to-know) for the implication.
Per channel, the connector stores:
A label for the channel in Frontic.
Project locale keys the CSV's rows resolve into. Every row gets the same payload across all of these.
The translation used as fallback for missing values.
For multi-locale data via CSV with locale-specific copy, see "Same row across all locales" below for the workaround.
## Data Feeds
The CSV Import exposes a single resource type — **Content** (one record per row). Each Data Feed binds a filename pattern under your base path; you can have many feeds per integration if you have many file types (products, customers, categories, …).
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For CSV Import specifically:
* **Updates step** — Polling and manual trigger are supported. The Ingest API isn't used.
* **No feed Refresh.** Each polling pass reprocesses every matching file. Use **Cleanup files** so processed files aren't picked up next time, or version filenames so the regex matches only new ones.
### Feed configuration
Subdirectory under the base path where this feed's files live. Empty = base path itself.
Field delimiter. Allowed values: `,`, `;`, `\t` — anything else is rejected.
Regex pattern matched against filenames in the start dir. For example `^products_\d{8}\.csv$` matches `products_20260426.csv`.
When on, before reading any file the connector checks for a lock file in the same folder. If a lock file exists, the run aborts — no files are processed. Useful for partner uploads that drop a `.lock` while writing.
*(Sub-config of **Use lockfile**.)* The exact filename to look for as the lock file. Defaults to empty (no check).
When on, files are processed in filename order. Useful when filenames carry timestamps and order matters.
*(Sub-config of **Sort files and folders**.)* Either `ASC` or `DESC`.
When on, the record's Source ID is built from one or more named columns instead of an `id` column. Multi-column identifiers are joined with `-`.
*(Sub-config of **Use custom identifier**.)* Column names to use as the Source ID.
*(Sub-config of **Use custom identifier**.)* Column names whose values become each record's parent identifier — joined with `-` when you list more than one. Use this when rows represent child records (e.g. product variants) and a column carries the parent's key. Leave empty when records have no parent.
Delete each file after a successful run.
*(Sub-config of **Cleanup files**.)* After cleaning files, also remove directories that ended up empty.
## What the data looks like
### One row, one record
The header row of the CSV becomes the field keys; each subsequent row becomes a record. Whitespace in headers is trimmed. The Source ID for a record is taken from the `id` column unless you've set **Custom identifiers**.
```csv theme={"theme":"css-variables"}
id;name;sku;price_eur;price_usd
1001;Cotton Tee;TEE-001;19.99;21.99
1002;Linen Shirt;SHI-002;49.99;54.99
```
The two rows above produce two records on the feed: `1001` and `1002`. As CSV files come in, the feed's schema fills in automatically from the column headers — no need to declare fields up front in the wizard's Schema step.
## Good to know
* **CSV only.** No JSON, no XML, no Excel. If you need other formats, convert upstream or use the [Custom Integration](/connectors/custom) and the [Ingest API](/reference/ingest-api).
* **Same row across all locales.** A CSV row produces the **same payload for every locale** on the channel. The CSV Import doesn't read column-name suffixes like `name_de` / `name_en` natively. To split by locale: ingest the fields raw and resolve translations in your [Value Composer](/data-integration/value-composer), or split your data into per-locale files and feeds and map them via the Data Sync.
* **No real-time.** The connector polls. Webhook-style "the moment a file arrives" delivery isn't supported — set the polling cadence for your acceptable lag.
* **Every run re-reads every matching file.** There's no file-level "I've seen this before" check — if a file stays in the directory, it's parsed again on the next run. Rows that haven't materially changed still short-circuit at the storage layer ([change detection](/connectors/overview#change-detection)), so downstream impact is contained — but each row counts as an API Update at intake. Use **Cleanup files** to delete processed files, or version filenames so the regex picks up only the new ones.
## Related
For source systems with their own API — push records directly via the Ingest API.
The HTTP push path when SFTP polling isn't the right fit.
# Custom Integration
Source: https://docs.frontic.com/connectors/custom
The Custom Integration is for source systems that don't have a first-party Frontic connector — or for cases where you want to push data straight into Frontic from your own code, scripts, or workflow tools. You define the payload shape, push it via the [Ingest API](/reference/ingest-api), and Frontic stores it on the feed as-is. No normalization happens on the connector side; reshaping into your storage schema is the Data Sync's job.
Auth
Ingest API token
Update method
Ingest API only
Resources
Content · Products · Categories
The Custom integration follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers what's specific to it.
## Connection settings
There's nothing to enter on the connection side — the Custom integration's "connection" is the [Ingest API](/reference/ingest-api). Authentication uses an Ingest API token.
## Channels
Channels for a Custom integration are pure containers for translations and (optionally) source-side channel keys you reference in payloads. There's no source system to validate against, so you define them yourself — Channel Name, Available Translations, Fallback Translation. See [Channels in the overview](/connectors/overview#channels) for the framework-level fields.
## Data Feeds
The Custom integration exposes three feed types:
| Feed | What it carries |
| -------------- | --------------------------------------------------------------------------------------- |
| **Content** | Generic records with the structure you define. |
| **Products** | Records with optional parent/variant linking via the configured parent-source-id field. |
| **Categories** | Generic records intended for category data. |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For Custom integrations specifically:
### Per-feed config
The name of the JSON key in your payload that carries the record's Source ID. Defaults to `id`.
*(Products feed only)* The name of the JSON key in your payload that carries the parent product's Source ID, used to link variants. Defaults to `parentId`.
### Update methods
* **Ingest API only.** Push records to the feed's HTTPS endpoint using the [Ingest API](/reference/ingest-api). Webhooks and scheduled polling don't apply here — your code controls when records arrive.
## Record structure
Records arrive as JSON. The connector recognizes three special keys for context-specific values: `@channel`, `@translation`, and `@currency`. You can use any combination on the same record — Frontic resolves them at sync time.
**Reserved key prefixes.** The `@`-prefix namespace above (`@channel`, `@translation`, `@currency`) and the `$`-prefix namespace used by other connectors (`$rel`, `$options`, `$properties` — see the [overview](/connectors/overview#reserved-prefix-namespace)) are both reserved for Frontic's framework. Don't use them as keys for your own custom data — they'll be interpreted or overwritten.
### Channel-specific data
Use `@channel` and the key of your integration's channels to define data that differs per channel.
```json theme={"theme":"css-variables"}
{
"id": 1,
"productNumber": "my-product-1234",
"name": "My Product",
"categoryIds": [14, 23, 30],
"@channel": {
"b2c": {
"technicalProductName": "My Product (Revision 241)"
},
"b2b": {
"technicalProductName": "AA-241-1234"
}
}
}
```
The value for `name` stays the same across channels; `technicalProductName` is per channel.
### Translation-specific data
Use `@translation` and a locale key for per-locale values:
```json theme={"theme":"css-variables"}
{
"id": 1,
"productNumber": "my-product-1234",
"categoryIds": [14, 23, 30],
"@translation":{
"de-DE":{
"name": "Produkt 1",
"description":"Ein Text"
},
"en-GB":{
"name": "Product 1",
"description":"Some Text"
}
}
}
```
### Currency-specific data
Use `@currency` and an ISO currency code for per-currency values (typically prices):
```json theme={"theme":"css-variables"}
{
"id": 1,
"productNumber": "my-product-1234",
"categoryIds": [14, 23, 30],
"@currency": {
"EUR": {
"productPrice": {
"gross": 19.99,
"net": 16.79,
"currency": "EUR",
"precision": 2
}
},
"USD": {
"productPrice": {
"gross": 19.99,
"net": 19.99,
"currency": "USD",
"precision": 2
}
}
}
}
```
### Combining annotations
You can combine `@channel`, `@translation`, and `@currency` on the same record. Frontic resolves them in the order **channel → currency → translation** so per-channel currency overrides apply correctly.
## Product variants
Products in a Custom integration support a parent-child structure. The variant relationship uses two field names from your payload: the **Source ID field** and the **Parent Source ID field**, both configurable on the feed.
There are three product types, distinguished by how the two fields relate:
Set parent Source ID equal to the Source ID — the record is its own parent.
```json theme={"theme":"css-variables"}
{
"id": "my-product-123",
"parentId": "my-product-123"
}
```
Set parent Source ID to an empty string. The record is the parent of its variants.
```json theme={"theme":"css-variables"}
{
"id": "my-product-456",
"parentId": ""
}
```
Set parent Source ID to the parent product's Source ID.
```json theme={"theme":"css-variables"}
{
"id": "my-product-123-red-small",
"parentId": "my-product-123"
}
```
Records can arrive in any order — variants before their parents are fine; Frontic links them once both are present.
## Bulk upserts
Push a JSON array to the Ingest API to upsert many records in one call:
```json theme={"theme":"css-variables"}
[
{
"id": "my-first-product"
},
{
"id": "my-second-product"
}
]
```
Each array element is treated as a separate record with the same rules as a single-record push.
## Troubleshooting
When records you've pushed aren't where you expect them to be, work through these checks in order:
Open the Data Feed and click **Recent Records**. The last records the connector accepted appear with their Source ID, timestamp, and full payload. If your latest push isn't there, the records didn't make it past intake — check the Ingest API response for a `400` or `401`, and confirm the Source ID field is present in every payload.
Click into a record. Verify the field names and shapes match what your Data Sync expects — typos in keys, accidentally-stringified numbers, or missing context annotations (`@channel`, `@translation`, `@currency`) are the usual culprits.
A feed without a [Data Sync](/data-integration/data-storages#populate-a-storage) connecting it to a storage stays a feed — records never become storage records. Confirm the sync exists and points at the right feed.
The Data Sync's **Channel → Scope mapping** decides which translations land in which project locale. Make sure the integration's channels are mapped to your scopes, and the channel's translations are mapped to the project locales — including the **fallback translation** the channel was configured with. Skipping the fallback is the single most common cause of "translations not syncing" issues with custom integrations.
If records reached the feed but a sync run produced a notification like *Storage Record Transformation Error*, the [Value Composer](/data-integration/value-composer) chain hit a runtime issue on a specific record. Open the **Record Debugger** in the feed sync panel to walk the chain on the offending record and pinpoint which slot produced the wrong value — see [Error handling](/reference/value-composer#error-handling) for the kinds of failures it surfaces.
## Good to know
* **You own the schema.** No source-side normalization happens — what you push is what arrives on the feed. Reshape into your storage schema in the [Value Composer](/data-integration/value-composer).
* **Records without a Source ID are dropped.** Confirm every payload carries the configured Source ID field; missing IDs are rejected silently at the feed level.
* **Change detection applies.** Re-pushing an identical record short-circuits past intake — see [change detection](/connectors/overview#change-detection) in the overview.
## Related
The HTTP endpoints, payload shape, and auth model.
Pushing into a Custom integration from an n8n workflow.
# n8n
Source: https://docs.frontic.com/connectors/n8n
There's no dedicated n8n connector — instead, n8n becomes a Frontic source by pushing records into a [Custom integration](/connectors/custom) via the [Ingest API](/reference/ingest-api). Use this when n8n is already orchestrating data between systems and you want to push enriched records to Frontic and no other connector is available.
Auth
Ingest API token
Update method
Ingest API (push from n8n)
Resources
Anything you push
## When to reach for n8n
Use this pattern when:
* You already run n8n to orchestrate data flow between systems and want Frontic to receive the enriched output.
* The source you want to ingest from doesn't have a first-party Frontic connector and you'd rather configure n8n than write code.
* You need a transformation step (deduplication, enrichment from another API, scheduled aggregation) before records get pushed to Frontic.
If your source is a system Frontic already connects to (Shopware, Shopify, Akeneo, …), use that connector directly — there's no advantage to routing through n8n.
## How it works
Create a [Custom integration](/connectors/custom) and add a Data Feed for the resource type you'll push. Note the feed's Ingest API endpoint and create an Ingest API token — see [Ingest API → Auth](/reference/ingest-api/upsert).
In n8n, build a workflow that produces records in the [Custom integration's payload shape](/connectors/custom#general-record-structure). Trigger it on a schedule, on webhook, or from any other n8n trigger node.
Use the **Frontic Ingest API** node in n8n to POST to the Frontic Ingest API endpoint. Set:
* **Ingest Operation**: `Create or Update` for adding data, `Delete` for removing records
* **Frontic Ingest API Credentials**: Choose your configured Frontic credentials form n8n
* **Payload**: For adding data, use the record JSON (or an array of records for bulk upsert). For removing records, pass an object with the record's ID (or an array of ID objects for bulk removal).
Run the workflow once to confirm records land in your Frontic feed, then activate it. From here on, every n8n run pushes its output into Frontic.
## What the data looks like
You define the record structure in n8n. Match the [Custom integration's record structure conventions](/connectors/custom#general-record-structure) — the connector recognises `@channel`, `@translation`, and `@currency` annotations for context-specific values.
## Good to know
* **You own the schema.** No normalization happens on the connector side — what you push is what's stored on the feed (reshaping into your storage schema is the Data Sync's job).
* **No webhooks back to n8n.** This is a one-way push from n8n into Frontic. If you need Frontic to notify n8n of stack changes, use a separate webhook source on the n8n side.
* **Quota counting.** Every push from n8n counts against your **API Updates** quota the same as any other Ingest API call. Schedule deliberately.
## Related
The Frontic-side configuration n8n posts to.
The HTTP endpoints, payload shape, and auth model.
# Overview
Source: https://docs.frontic.com/connectors/overview
A **connector** is a piece of Frontic that knows how to talk to a specific source system — be it Shopware, Shopify, commercetools, some other system or even a CSV file drop. The connector handles the source's quirks for you — splitting inline variants, resolving translations, normalizing prices — so the records that land on your Data Feeds are already shaped for Frontic. You use a connector to set up an **integration**: a configured, credentialed link in your Frontic project that pulls data in (or accepts data pushed at it).
This page covers how integrations work end-to-end. For vendor-specific setup, click into a connector below.
## What's in an integration
Auth and host for one source system. Valid options come from the source itself.
Translations the channel covers, plus connector-specific source segmentation where applicable.
One feed per resource type, each with assigned channels and an update method.
## How data flows from source to project
Your Shopware store, Shopify shop, CSV file drop, ERP — whatever's authoritative for the data.
The connector's link to that source. Auth, host, channel config — everything that decides *what* to pull and *from where*.
Live records inside Frontic, one feed per resource type. The connector keeps these in sync with the source via the feed's update methods.
The mapping layer that decides how feed records become storage records — including which channel powers which project scope, which channel locale fills which project locale, and how the [Value Composer](/data-integration/value-composer) reshapes each field.
The semantically structured records your blocks, listings, and pages query at runtime.
The integration owns "what comes from the source" — the project's scopes, regions, and locales are independent. The Data Sync's mappings are where the two models meet.
## Connection
Every integration carries one set of credentials to talk to one source system. Connection settings sit at the integration level — host URL, API keys, OAuth tokens, SFTP credentials, whatever the source needs. Frontic stores secrets encrypted and redacts them in logs.
Once connected, the connector pulls **reference data** from the source — typically the list of valid sales channels, currencies, and languages — and uses it to populate dropdowns when you configure channels or feeds.
A **Refresh** action is available wherever reference data is shown, in case you've added a sales channel or locale on the source side and want Frontic to pick it up without recreating the integration.
### IP Pool
Depending on your integration, Frontic will need to connect to your systems for fetching data.
We will use this IP pool for connections:
* `195.201.46.231`
* `78.47.233.107`
## Channels
Every channel carries fields that describe which translations it covers:
* **Channel name** — your label.
* **Available translations** — the locales this channel covers.
* **Fallback translation** — used when a translation is missing on a record.
On top of those, a connector can declare additional **typed system fields** on its channels — name, type, required flag, and a reference to a validated set of possible values pulled from the source. These aren't free-form metadata: the framework enforces them just like the framework-level fields, and the dropdowns are populated from the source.
* **Shopware** adds **Sales Channel** (validated against the live list of Shopware sales channels) and **Currencies** (validated against the sales channel's currencies).
* **commercetools** and **Akeneo** each add **Currencies**, validated against the source's enabled currency set.
* Other integrations may not declare any additional channel fields today — their channels carry only the basic fields, and any source-side segmentation lives at the connection or feed level instead.
The same Frontic project can host as many channels as you have segmentations to pull from.
## Data Feeds
A Data Feed is a sync source for one resource type. The connector declares which feed types it supports — Shopware for example exposes Products, Categories, and CMS Pages; Reviews.io exposes Product Reviews and Company Reviews; the File Importer simply exposes Content. You'll see only the feed types relevant to the connector you picked.
Each feed fetches data from all your channels — a Shopware Products feed, for instance, runs against every channel you've assigned it to, pulling the catalog for each (sales channel × currencies × translations) combination.
### The setup wizard
Adding a feed walks through three steps, with a **Finish** indicator on the progress bar marking the completion state:
Name the feed and pick its resource type from the connector's available types.
Configure the update methods this feed uses to stay in sync (see below). Only the methods the connector supports are offered.
Optionally pre-declare the feed's fields. Leave it empty if you'd rather let the schema fill in automatically as records arrive — every incoming record extends the feed's schema with any new fields it carries.
Each feed has a **Refresh** action that re-runs it against the source — useful when something changed on the source side that didn't reach Frontic, or to recover from a webhook gap. You don't need to refresh after a configuration change like updating the Value Composer, the storage schema, or the sync's channel/locale mappings — those revalidate automatically (see [change detection](#change-detection) below).
## What the connector does for you
A connector isn't a passthrough. Each adapter normalizes its source's quirks before records reach your Data Sync — so what arrives is already in a shape your Value Composer can map cleanly. What that means varies by connector:
Where the source mixes parent products and their variants in one structure, the connector splits them into separate records linked by `parentId` — so blocks drive grids off products and detail off variants.
When the source carries multi-currency pricing, the connector pre-filters the price array to the channel's currencies and stamps `currencyCode` and `currencyPrecision` on each entry.
Source-side localized payloads are resolved to the right value per channel translation. Your sync references field names directly — no walking source-specific translation structures.
Where a connector supports it, a refresh diffs the previous result against the source's current state and emits deletes for records that disappeared — your storage doesn't accumulate orphans.
For exact per-connector behavior — which associations are pre-loaded, where translations fall back, what auto-delete covers — see each connector's "What the data looks like" section.
### Reserved `$`-prefix namespace
When a connector normalizes source data, it sometimes stamps the result onto the feed record under a **reserved `$`-prefix key** so it sits next to your record's own fields without colliding with them. For example:
* **`$rel`** — relation metadata (Shopware uses this to expose a flat list of category IDs as `$rel.categoryIds` so syncs don't have to walk the full association)
* **`$options`** — variant-defining attributes
* **`$properties`** — descriptive attributes that don't drive variant selection
## Update methods
Frontic supports three update methods for keeping feeds in sync. Each connector declares which combinations it supports — some support all three, some only a subset.
The source pushes a webhook to Frontic on every change — near-real-time delivery for time-sensitive catalogs.
Frontic pulls from the source on a schedule. Lower bandwidth than triggers, with a built-in safety net behind them.
The source pushes records into Frontic's [Ingest API](/reference/ingest-api). You control timing, batching, and payload shape from your code.
A connector can support more than one method, and you pick which ones to enable per feed. If possible, a production setup should combine **trigger + polling** — webhooks for low-latency updates, polling as a backstop for missed events.
## Change detection
When a record arrives at Frontic — through any update method — the connector takes a fingerprint of it and compares against the version already on file. If nothing has materially changed, the write is dropped early: **no storage update, no cache to purge in your frontend, no downstream re-render**. Intake itself still happens (and still counts as an **API Update**), but real updates further down the queue don't get held up by no-op churn.
If you control the source, prefer to push only when something has actually changed — that's always cheapest. If you can't (a nightly file drop with yesterday's content, a webhook fired by an unrelated edit), the fingerprint check keeps the downstream impact minimal.
When you change a configuration like the Value Composer, the storage schema, or the sync's channel/locale mappings, the fingerprint shifts on every affected record — Frontic re-emits them automatically, so the new shape lands downstream without a manual refresh.
## Data Syncs — mapping integration to project
Up to this point, everything is integration-side. The Data Sync is where the integration meets your project.
A Data Sync sits between a feed and a storage and carries two kinds of mappings:
For every project scope, pick which integration channel powers it. The same channel can power many scopes (one Shopware sales channel feeding both `public` and `b2b` scopes), or different scopes can be powered by different channels (one channel per scope).
For every project locale within a scope, pick which translation from the mapped channel fills it. The same channel translation can map to many project locales (a single `de` translation feeding `de-DE`, `de-AT`, and `de-CH`).
Both mappings are constrained: **every project scope and every locale within that scope must be satisfied** before the Data Sync can run. Frontic validates this at save time — you can't end up with a project scope or locale silently missing data.
Where a project locale's mapped channel translation has no value for a record, Frontic falls back to the channel's **fallback translation** before returning `null`. That makes partially-translated catalogues safe to ship: the experience still renders, it just renders the fallback locale's value where translation hasn't caught up.
## Available connectors
Shopify Admin API connector for products, blogs, pages, and metaobjects.
Shopware 6 connector with companion plugin, end-to-end onboarding, and webhook updates.
commercetools connector for products and categories via OAuth 2.0.
Akeneo PIM connector for product attributes, families, and categories.
Contentful connector for editorial content with environment selection.
Storyblok connector for stories with draft / published mode toggle.
Reviews.io connector for product and company reviews.
n8n recipe pack for Frontic — push records via the Ingest API from any n8n workflow.
SFTP-based CSV connector for nightly exports and partner-supplied feeds.
For source systems without a first-party connector — push any JSON shape via the Ingest API.
The Reviews.io connector syncs **product reviews** and **company reviews** from Reviews.io into Frontic — ratings, review bodies, reviewer metadata, and verification flags all come through ready to power product detail pages, social proof modules, and review listings.
Auth
Store key
Update methods
Trigger
Resources
Product · Company reviews
## What you get
Per-product reviews — rating, body, reviewer name, verification status. Link them to your product records via the reviewed product's identifier.
Reviews of your store as a whole — useful for trust badges, store-page testimonials, and aggregate rating displays.
The Reviews.io connector follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds.
## Connection settings
Your Reviews.io store key. Find it in Reviews.io under **Integrations → API**. The connector authenticates against the Reviews.io API with this key.
## Channels
A Reviews.io channel carries which translations the connector pulls per review.
A label for the channel in Frontic.
Locale keys the connector applies to each review.
The translation used as fallback for missing values.
## Data Feeds
The Reviews.io connector exposes two feed types:
| Feed | What it pulls |
| ------------------- | ----------------------------------------------------------------------------------- |
| **Product Reviews** | Reviews tied to specific products. Source ID is the review's Reviews.io identifier. |
| **Company Reviews** | Reviews of your store. Source ID is the review's Reviews.io identifier. |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). All three update methods are supported:
* **Trigger** — Reviews.io webhooks land at the feed's inbound URL for real-time updates.
The connector does not expose a manual feed Refresh — push via webhook.
## What the data looks like
Reviews come through with the Reviews.io payload shape — ratings, body, reviewer details, photos, replies, and timestamps — preserved on the raw payload. Map fields into your storage in the [Value Composer](/data-integration/value-composer).
For linking product reviews to your product records, the reviewed product's identifier in the Reviews.io payload (typically SKU or product ID) is what you'll match against your Products storage's keys.
## Good to know
* **Read-only.** The Reviews.io connector pulls reviews from Reviews.io; it doesn't push back. Review collection and moderation happen in Reviews.io.
* **Aggregate ratings.** The connector emits raw reviews. Aggregate rating values (average score, total count per product) are computed in your Data Sync or at query time.
## Related
For displaying reviews ranked, filtered, and paginated on your storefront.
For pushing reviews from systems Reviews.io doesn't already cover.
# Shopify
Source: https://docs.frontic.com/connectors/shopify
The Shopify connector syncs your Shopify store's catalog and content into Frontic. Variants are denormalized into separate records with parent links preserved, and the connector covers four resource types — products, blogs, pages, and metaobjects — so you can build product, content, and structured-data experiences off the same backbone.
Auth
Access Token
Update methods
Polling
Resources
Products, Blog, Pages, Metaobjects
## What you get
Variants split into separate records, parent links preserved. Per-variant fetch fills in inventory and price detail.
Blog articles by blog ID — pick which blog feeds the storage when you create the feed.
Shopify pages — about, contact, custom marketing pages.
Shopify metaobjects for structured custom data — lookbooks, ingredient lists, store locations.
The Shopify connector follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers the Shopify-specific configuration.
## Connection settings
Your Shopify store domain, e.g. `https://my-shop.myshopify.com`.
The Shopify app's API key.
The Shopify app's API secret key.
The admin API access token granted to your app for the store.
The settings panel shows a **Connection Status** indicator. Shopify doesn't expose a dedicated connection-test endpoint, so the status stays at `unknown` until the first sync runs successfully — at which point it flips to `connected`. Auth failures surface their reason on the indicator after a sync attempt.
## Channels
A Shopify channel in Frontic carries which translations the connector pulls per record.
A label for the channel in Frontic.
Locale keys the connector pulls per record.
The translation used when a value is missing in another locale.
## Data Feeds
The Shopify connector exposes four feed types:
| Feed | What it pulls | Per-feed config |
| --------------- | ------------------------------------------------------------------------------------- | -------------------- |
| **Products** | All products with variants split into separate records, parent-child links preserved. | — |
| **Blog** | Articles from a specific blog. | `Blog ID` (required) |
| **Pages** | Shopify pages. | — |
| **Metaobjects** | Structured custom data — lookbooks, store locations, etc. | — |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds).
### Blog feed config
The Shopify blog ID this feed reads from. Find it in Shopify admin under **Online Store → Blog posts** — the blog's ID is in the URL when you open the blog's settings.
## What the data looks like
### Variants
Shopify always exposes products and variants as a parent/children pair, so the connector mirrors that: every product becomes a parent record (empty `parentId`), and every variant becomes its own record with `parentId` set to the product. **Even single-variant products produce two records** — one parent plus one variant — because that's the structure Shopify itself returns. See [Product Models](/commerce-concepts/product-models) for how this composes at the API layer.
For each variant, the connector issues a **dedicated Shopify API call** to fetch full variant detail (inventory, pricing, options) and merges it into the variant record on top of the parent's payload. This makes large catalogs cost more requests against Shopify's rate limit than a flat product fetch would — keep it in mind when configuring polling schedules on bigger stores.
### Schema auto-derivation
Shopify product payloads are rich JSON. As records arrive on the feed, Frontic's auto-schema fills in the field list for you — no need to declare every Shopify field up front in the wizard's Schema step. See [feed schema](/connectors/overview#data-feeds) in the overview.
## Good to know
* **No companion app.** Setup is direct: create a Shopify app or private API client, copy the credentials in. There's no Frontic-side app you install in Shopify.
* **Per-variant fetch.** Fetching variant detail issues one request per variant under the hood — large catalogs benefit from polling schedules on the Shopify rate-limit side.
* **Custom fields & metafields.** Standard product fields come through resolved; metafields land on the raw payload. Map them in your Data Sync.
## Related
How Shopify's parent products and variants compose at the API layer.
A fully-fledged commerce-platform connector — companion plugin, end-to-end onboarding.
# Shopware
Source: https://docs.frontic.com/connectors/shopware
The Shopware connector pairs your Shopware 6 store with Frontic via a **Frontic plugin you install in your Shopware admin**. The plugin handles the hand-off both ways: it pushes catalog and content changes to Frontic via webhooks, and at install time it calls back into Frontic to provision a project that mirrors your store's sales channels, currencies, and languages — feeds, storages, and syncs included.
Auth
API Key + Secret
Update method
Trigger
Resources
Products, Categories, CMS
## What you get
Every variant as its own record, parent links preserved, prices resolved to the currency.
Power category pages, attach category context to other records, drive navigation.
Shopping Experiences and Landing Pages with their section/block/element structure intact.
The Shopware connector follows the standard integration model — see [how integrations work](/connectors/overview) for the shape of Connection, Channels, and Data Feeds. This page covers the Shopware-specific pieces: the plugin install, the onboarding hand-off, the channel and feed details, and the data shape after normalization.
## Install and setup
The plugin is the same either way — pick how you'd like to load it into Shopware, then run the onboarding to provision your Frontic project.
### Install the plugin
In Shopware admin, go to **Extensions → Store**. You'll need a Shopware Account connected — Shopware prompts you to connect one if you haven't.
Search for **Frontic** in the store and open the listing.
Install and activate the plugin in Shopware's extension overview.
Grab the latest release ZIP: [Frontic plugin for Shopware 6](https://github.com/frontic/shopware6-plugin/releases/latest).
In Shopware admin, go to **Extensions → My extensions** and click **Upload extension**. Pick the ZIP you just downloaded.
Install and activate the plugin in Shopware's extension overview.
### Run the onboarding
With the plugin active, you can start the onboarding wizard on the plugin's configuration page to provision your Frontic project end-to-end: it sets up the integration's connection settings, creates one **Channel** per Shopware sales channel (with the matching currencies and translations), creates Data Feeds for Products, Categories, and CMS Pages, and seeds matching Data Storages and Data Syncs. On the project side it also creates one **scope** per sales channel, one **region** per channel/currency pair, and one **locale** per channel language.
Before any of that runs, the wizard tests the Shopware connection from Frontic — if the keys are wrong or the host can't be reached, it stops on a **Connection-Test failed** screen instead of provisioning a broken setup. See [Troubleshooting the onboarding](#troubleshooting-the-onboarding) for the failure modes.
In **Extensions → My extensions**, click the Frontic row's three-dot menu and choose **Configure**. Start the Onboarding wizard with the **Start Onboarding** button.
The wizard opens a Frontic auth window. Sign in or create the account that should own the new project, then approve the plugin's permission request.
Review the proposed scopes, regions, currencies, and locales mapped from your sales channels, then click **Create project**.
The plugin creates Data Feeds for products, categories, and CMS pages and triggers an initial backfill. From here on, Shopware changes flow to Frontic on every entity write.
### Troubleshooting the onboarding
When the connection check fails, the wizard shows a **Connection-Test failed** screen with the message Shopware returned and a failure type. Match it against the table below to find the fix — once the test passes, the wizard continues to provisioning automatically.
| Failure | What it means | How to fix it |
| ------------------------------------ | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_credentials` (401) | Shopware rejected the keys the wizard generated. | Restart the wizard so it can issue fresh keys. If that still fails, fall back to the [manual Integration account](#without-the-plugin) flow. |
| `insufficient_permissions` (403) | The Integration account exists but is missing read permissions. | Open the Integration in Shopware and assign the role described in [Create an Integration account in Shopware](#without-the-plugin). |
| `api_not_found` (404) | The host URL doesn't point at the Shopware Admin API. | Use the Shopware admin URL (the one you use to log in), not a storefront URL or a custom subpath. |
| `network_error` | Frontic couldn't reach the host from the public internet. | Make the Shopware host publicly reachable. Local-only or IP-allowlisted instances can't be onboarded automatically. |
| `rate_limited` (429) | Shopware throttled the verification request. | Wait a moment and retry the wizard. |
| `shopware_unavailable` (502/503/504) | Shopware returned a temporary error — often a deploy or upstream outage. | Wait until the instance is healthy, then retry. |
### Without the plugin
If you can't install the plugin in your Shopware environment — for example a managed setup where you can't load custom extensions — you can connect manually using a Shopware **Integration** account.
Without the plugin, Frontic will not be notified of any data changes. You have to trigger updates manually.
In Shopware admin, go to **Settings → System → Integrations** and click **Add integration**.
Enter `frontic-sync` (or anything you'll recognize later).
Assign a role with **read** access on: Product, Category, Sales channel, Currency, Language, Media, Manufacturers, Properties, Shopping Experiences, Landing Pages, Delivery times, Essential Characteristics, Scale Units, Tags, Tax. Plus read on the `*_translation` detailed privileges for product, media, manufacturer, property group, property group option, unit, delivery time, cross-selling, and feature set.
Save the integration, then copy the **Access Key ID** (`SWIAMG…`) and **Secret Access Key** (`dXhZQ2…`) — you'll paste them into Frontic next.
In Frontic admin, **Integrations → Add → Shopware** asks for the host URL plus those two keys. With manual setup, you also build out the project (scopes/regions/locales) yourself in Project Settings — see [Multi-Channel & Multi-Region](/commerce-concepts/multi-channel-region) — and create channels and feeds manually as covered below.
## Connection settings
Display name for the integration in Frontic. Use something recognizable when you have multiple Shopware integrations (e.g. `Shopware Production`, `Shopware Staging`).
The URL of your Shopware instance, e.g. `https://shopware.demo-shop.com`. The connector talks to the Admin API at this host.
The **Access Key ID** of a Shopware Integration account (`SWIAMG…`). Created automatically by the plugin onboarding, or manually via Shopware **Settings → System → Integrations**.
The **Secret Access Key** paired with the API key (`dXhZQ2…`). Stored encrypted and redacted in logs.
The settings panel shows a **Connection Status** indicator. When the connection is broken, it surfaces the reason returned by the Shopware API (with sensitive details redacted) — so you can tell whether credentials are missing, the host is unreachable, or the keys were rejected. Update credentials here and re-test after rotating keys in Shopware.
## Channels
A Shopware channel in Frontic maps a **Shopware sales channel** to the currencies and translations Frontic should pull from it. The plugin onboarding seeds one Frontic channel per sales channel; you can add more from **Channels → Add**.
A label for the channel in Frontic (e.g. `Storefront`, `Headless`, `Wholesale`).
Locale keys this channel covers. Validated against the languages the chosen Shopware sales channel actually serves; populated from the connector's reference data.
The locale used as fallback when a record is missing a value in another translation.
Which Shopware sales channel this Frontic channel reads from. The dropdown is populated from your Shopware instance — hit **Refresh** if you've added a new sales channel since the integration last fetched.
This selection also narrows product ingestion: a product needs a visibility entry for one of the sales channels selected across the integration's channels to be fetched. See [Product visibility](#product-visibility).
Currencies (from the chosen sales channel) the connector pulls in. Product prices are pre-filtered to these currencies before they reach the Data Sync.
The **Refresh** button on the channel detail re-fetches sales channels, languages, and currencies from your Shopware instance — use it after you've added one of those on the Shopware side. For how channels become project scopes and locales at sync time, see [Data Sync mapping in the overview](/connectors/overview#data-syncs-mapping-integration-to-project).
## Data Feeds
The Shopware connector exposes three feed types:
| Feed | What it pulls | Webhook events |
| -------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------- |
| **Products** | Catalog including all variants and their prices | `product.written`, `product.deleted` |
| **Categories** | The category tree, including SEO and assets | `category.written`, `category.deleted` |
| **CMS Pages** | Shopping Experiences and Landing Pages | `cms_page.written`, `cms_page.deleted`, `landing_page.written`, `landing_page.deleted` |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For Shopware specifically:
* **Updates step** — Shopware supports the **Trigger** update method (the plugin pushes webhooks). Scheduled polling and the Ingest API are not used; the plugin's webhook stream is the source of real-time updates.
* **Manual refresh** — every Shopware feed exposes a **Refresh** action that re-runs the feed against the source. Use it when something changed on the Shopware side that didn't reach Frontic, or to recover from a webhook gap. Configuration changes — Value Composer, storage schema, sync mappings — don't need a refresh; they revalidate automatically.
### Product visibility
The Shopware integration only ingests products with a visibility entry for at least one of the sales channels selected across its channels. Products with no visibility — or visible only in sales channels you haven't selected on any channel — won't reach Frontic.
The filter applies to every product read: full backfills, parent resolution for variants, and incremental batch fetches. A product that drops out of every selected sales channel in Shopware is reconciled away on the next feed run.
If a product is missing in Frontic, open it in Shopware admin and check the **Visibilities** tab — it needs an active entry for one of the sales channels selected on a channel in this integration.
## What the data looks like
### Associations pulled along
The connector fetches more than just the entity's own fields — it pre-loads the associations a typical storefront needs, so your Data Sync doesn't have to chase them.
`categoriesRo`, `cover.media`, `crossSellings`, `crossSellingAssignedProducts`, `deliveryTime`, `featureSet`, `manufacturer.media`, `prices`, `media`, `options.group`, `properties.group`, `tags`, `unit`, `visibilities`.
`media`, `tags`.
Need an association that isn't on this list? [Get in touch](mailto:support@frontic.com).
### Variants
A product with options is denormalized: the parent record carries the shared fields, each variant is a separate record linked back via `parentId`. Either side can drive a [Detail Block](/api-builder/blocks) — product-level for grids, variant-level for cart and detail views. See [Product Models](/commerce-concepts/product-models) for the full picture.
### Currency-resolved prices
For products, the connector pre-filters the `prices` array to the currencies declared on the channel and stamps every price element with `currencyCode` and `currencyPrecision`. Map those into the [Price composite](/reference/storage-field-types#composites) and you're done — no per-region price-picking logic in your Value Composer needed.
### Translations
All translatable fields are resolved at the connector level for every locale on the channel. Use the **base field** in your Data Sync (`name`, `description`) — not `translated.name`. Frontic's Request Context picks the right localized value at response time. For categories, breadcrumb arrays are also de-indexed so the translated breadcrumb path is a clean list rather than a `0:..., 1:...` map.
### Auto-delete reconciliation
When a feed runs (refresh, full backfill), the connector diffs Shopware's current state against what Frontic has on the feed and emits **deletes** for products, categories, or CMS pages that have disappeared on the Shopware side. Your storage doesn't accumulate orphan records when items are removed in Shopware.
### Category relations
From the `categoriesRo` association, the connector also extracts a flat list of category IDs and stamps them onto the feed record under the reserved `$rel` namespace as `$rel.categoryIds`. Category-link blocks (or any sync that just needs the IDs without the full association payload) can map this directly in the Value Composer — no need to walk `categoriesRo` to pluck IDs out yourself.
## Good to know
* **Products are scoped by sales channel visibility.** Only products visible in one of the sales channels selected across the integration's channels are fetched — see [Product visibility](#product-visibility).
* **No scheduled polling.** Shopware sync relies entirely on plugin webhooks. If the plugin is disabled or your instance can't reach Frontic, changes won't be pushed — fix connectivity, then hit **Refresh** on each feed to backfill.
* **Webhook delivery retry is on the plugin side.** The Frontic plugin queues outbound webhooks in Shopware and retries delivery — events older than Shopware's retention can't be replayed and need a manual feed Refresh to reach Frontic.
* **Batch size.** Records are fetched in batches to keep Shopware's API response size small enough for catalogs with large associations.
## Related
How Shopware's parent products and variants compose at the API layer.
Shopify's commerce-platform connector — direct API, four resource types.
# Storyblok
Source: https://docs.frontic.com/connectors/storyblok
The Storyblok connector syncs stories from a Storyblok space into Frontic. It supports **draft and published modes**, **folder-based filtering** for selective imports, and **auto-deletion** of stories removed in Storyblok — so your Frontic content stays in sync with the source as it changes.
Auth
Access token
Update methods
Trigger · Polling
Resources
Stories
The Storyblok connector follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers the Storyblok-specific configuration.
## Connection settings
Your Storyblok space ID. Find it in Storyblok under **Settings → Space → General**.
A Content Delivery API token for your space. Use a draft token if you've enabled draft mode (below); otherwise a public token suffices.
When on, the connector reads from Storyblok's draft API — pulling unpublished stories alongside published ones. Useful for preview environments. Off by default; production-style integrations should leave this off.
The connection populates the channel's available languages from your space's configured locales.
## Channels
A Storyblok channel in Frontic carries which translations the connector pulls per story.
A label for the channel in Frontic.
Storyblok locale codes the connector pulls per story.
The translation used when a story field is missing in another locale.
The **Refresh** button re-fetches your space's configured languages — use it after enabling new locales in Storyblok.
## Data Feeds
The Storyblok connector exposes one feed type:
| Feed | What it pulls |
| ----------- | -------------------------------------------------------------------------------------------------------- |
| **Content** | Stories from your space — every story across every configured language is pulled and merged by story ID. |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For Storyblok specifically:
### Per-feed config
Optional list of folder slugs to restrict the feed to. When set, only stories under these folders are pulled — useful when one Storyblok space serves multiple Frontic projects.
### Update methods
* **Trigger** — Storyblok webhooks (Settings → Webhooks) push story updates and deletions to the feed's inbound URL in real-time.
* **Polling** — scheduled refresh.
The Ingest API isn't used by Storyblok. Each Storyblok feed exposes a manual **Refresh** action.
## What the data looks like
### Stories merged across languages
Stories in Storyblok are stored once and translated via the language layer. The connector merges all language variants of a story into a single record set, then emits one record per (channel translation, story) pair. The Source ID is the story's Storyblok ID.
### Folder filtering
When **Folders** is set on a feed, the connector applies the allow-list against the default-language pass — stories not in any allowed folder are skipped before translation merging.
### Auto-deletion
When a refresh runs, the connector reconciles its previous result against the current state of Storyblok. Stories that have been **deleted in Storyblok** are deleted in Frontic too — so you don't end up with orphan content. This is unique to Storyblok among Frontic's connectors today.
### Draft vs. published
When **Draft Mode** is on at the connection level, the connector pulls drafts alongside published stories. Use this for preview environments; combine with a separate Frontic project (or scope) so drafts don't leak into production.
## Good to know
* **Auto-deletion is the connector's behavior.** The connector reconciles aggressively against Storyblok's current state.
* **Folder allow-list applies at the default-language pass.** If a story exists in a non-default language but its default-language version is excluded by folder filter, the non-default language version is skipped too.
* **Custom field types.** Storyblok's custom field types come through on the raw payload. Resolve them in your Value Composer if you need them as first-class fields.
## Related
Contentful's CMS connector — environment selection.
For pushing supplemental records via the Ingest API.
# Xentral
Source: https://docs.frontic.com/connectors/xentral
The Xentral connector syncs your ERP's product catalog and category tree (also known as item tree or Article tree or Artikelbaum) into Frontic. Products are normalized with texts, prices, media, stock levels, and properties pre-loaded per record — so your Data Sync receives a complete, structured payload.
Auth
Personal Access Token
Update method
Polling
Resources
Products, Categories
## What you get
Matrix products as parent records, variants as children — with texts, prices, media, stock, and properties pre-loaded per record.
Power category pages, attach category context to product records, and drive navigation.
## Connection settings
Your Xentral instance subdomain — the part before `.xentral.biz`. For `my-company.xentral.biz`, enter `my-company`.
A Xentral API personal access token with read-only Product permissions. Generate one in Xentral under **Account Settings → Developer Settings → Personal Access Tokens**. Stored encrypted-at-rest.
The settings panel shows a **Connection Status** indicator. When the connection is broken it surfaces the reason returned by the Xentral API — so you can tell whether the token is invalid, expired, or the Xentral instance is unreachable. Update credentials here and re-test after the issue is fixed.
## Channels
A Xentral channel in Frontic has a selection of currencies available. Translations are populated from the translations Xentral exposes per locale.
A label for the channel in Frontic.
Locale keys this channel covers. Populated from Xentral's locales.
The translation used when a value is missing in another locale.
Currencies the connector pulls in. Product prices are pre-filtered to these currencies before they reach the Data Sync — only the sales prices valid today for the chosen currencies are included.
## Data Feeds
The Xentral connector exposes two feed types:
| Feed | What it pulls |
| -------------- | ------------------------------------------------------------------------------------------------------------- |
| **Products** | All products and variants with texts, prices, media, stock levels, properties, and cross-sellings pre-loaded. |
| **Categories** | The full product category tree, parent links preserved. |
The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For Xentral specifically:
* **Updates step** — Polling is supported. On the first run (or after a manual refresh), all records are fetched regardless of timestamp.
* **Manual refresh** — every Xentral feed exposes a **Refresh** action that re-runs the feed against the source in full. Use it when something changed on the Xentral side that didn't reach Frontic during a scheduled poll, or to recover after a polling gap.
If you're using prices limited by a date range, make sure to update the product feed at least daily to ensure correct price availability.
### Product Feed
For your products, you can optionally choose to prefer prices for a specific Xentral group, falling back to the default price. Otherwise, a default price will be used.
The Xentral group to be used for price selection, if available.
## What the data looks like
### Variants
Xentral distinguishes between **matrix products** (configurable products with variants) and **simple products** (no variant axis). The connector handles both:
* **Matrix products** — the matrix product record becomes the parent, each variant becomes a separate record with `parentId` pointing back to the matrix product.
* **Simple products** — emitted as a self-parented record using the same source ID on both sides. See [Product Models](/commerce-concepts/product-models) for how this shape composes at the API layer.
### Currency-resolved prices
For products, the connector pre-filters sales prices to the currencies declared on the channel. Only prices valid on the current date that apply to all customers (no customer-specific or customer-group-specific pricing) are included. Map them into the [Price composite](/reference/storage-field-types#composites) in the Value Composer — no per-region price-picking logic needed.
### Translations
Product texts are fetched per locale. The connector resolves the localized text values (name, description, and other translatable fields from Xentral's API) for every locale on the channel.
The Xentral API does not expose translations for categories and some of the product data (like properties). We're providing everything we can get via the Xentral API.
### Attributes split into options and properties
The connector's attribute extractor separates variant-defining options from descriptive properties and stamps them onto the feed record under the reserved `$`-prefix namespace:
* **`$options`** — the `selectedOptions` from variant records, normalized into a flat list per record. Drives variant selectors (size, colour, etc.).
* **`$properties`** — product property values with their property name, value, and unit. Descriptive attributes that don't drive variant selection.
Map either list into your storage with the [Option composite](/reference/storage-field-types#composites) in the Value Composer, rather than deriving them from Xentral's raw structures.
### Additional data pre-loaded
Each product record also carries:
* **`$medias`** — product media items (images, documents).
* **`$stocks`** — stock levels per warehouse location.
* **`$crossSellings`** — cross-selling product references.
### Category relations
The connector adds a flat list of category IDs onto each product record under `$rel.categoryIds`, derived from the product's `categories` association. Category-link blocks and syncs that need only the IDs can map this directly in the Value Composer.
### Auto-delete reconciliation
When a feed runs in full (manual refresh), the connector diffs Xentral's current record set against what Frontic holds and emits **deletes** for products or categories that have disappeared on the Xentral side. Your storage doesn't accumulate orphan records when items are removed in Xentral.
## Good to know
* **Read-only.** The Xentral connector pulls from Xentral; it doesn't push back. Catalog edits happen in Xentral.
* **Incremental polling.** Scheduled polls fetch only products updated since the last successful poll — keeping regular syncs fast on large catalogs. Categories are always fetched in full on each poll.
* **Reserved `$`-prefix keys.** The connector emits `$options`, `$properties`, `$medias`, `$stocks`, and `$crossSellings` on every product record. Don't use `$`-prefixed keys for your own custom attributes — Frontic reserves that namespace for connector-side normalization.
## Related
How Xentral's matrix products and variants compose at the API layer.
Same `$options` / `$properties` attribute split.
# Commands
Source: https://docs.frontic.com/context-base/commands
A **command** in the [Context Base](/context-base/overview) is a named operation the agent runs when you invoke it by name. Commands share the same shape as [skills](/context-base/skills) — the content body is the same kind of instruction — but there's one crucial difference: **the agent never auto-picks a command.** It runs only when you ask for it.
Use commands for work that's too narrow or too invasive to auto-apply: a `code-review`, a `test-checkout`, a `verify-accessibility` — things you *sometimes* want to run and don't want the agent starting on its own.
Commands currently don't sync to your local repo via `frontic context init`.
Only [skills](/context-base/skills) and [rules](/context-base/rules) are
exported to `.claude/` and `.cursor/` today. You invoke commands from the
Frontic surfaces (Studio chat, MCP in your editor) where the agent can
read them directly from the project's Context Base.
## How commands fit with the other Context Base types
The four Context Base types compared:
| | Triggered by | Shape | Example |
| ------------ | ------------------------------------- | -------------------- | ---------------------------- |
| **Commands** | Forced by name only | Task procedure | `code-review` |
| **Skills** | A matching domain (or forced by name) | Domain expertise | `seo-optimizer` |
| **Rules** | Always active | Always-on constraint | `product-images-through-cdn` |
| **Guides** | Explicitly referenced | Reference document | `authentication-flow` |
A skill is knowledge, a rule is a constraint, a guide is reference material — all three are things the agent *reads*. A command is the only one that's an *action*. You ask for a skill implicitly (by describing the task) or explicitly (by naming it); you invoke a command by name to have the agent run its defined flow.
## What commands are good for
`code-review`, `verify-accessibility`, `check-performance` —
things you run when you want them, not every time the agent
touches the codebase. Invoking them explicitly keeps the agent
from second-guessing.
`build-landing-page`, `scaffold-product-detail-page`,
`add-block-component` — the team's way of creating something you
put together often enough that "build me one of those" should
always land the same shape.
The common pattern: **work you want done a specific way when you ask for it, not every time the agent looks at a task**.
## Writing a command
Open **Studio → Context → Commands** in the admin app and click **New Command**.
Creation asks for two things:
* **Name** — short, memorable, triggerable. `code-review` and `build-landing-page` are good.
* **Description** — one sentence for the commands list.
Once the command is created, you land in the editor where you write the operation itself in Markdown — the steps in order, with enough detail that the agent doesn't have to guess. New commands start as **Draft** — flip the status to **Active** when the content is ready to run.
Commands can be scoped to this project (the default) or [promoted to your organization](/context-base/overview#document-scoping-project-vs-global) so they apply across every project on your team.
The command body is the operation itself. Unlike skills (domain expertise) and rules (constraints), the command body is an actionable procedure the agent is expected to execute when triggered.
## Example: build-landing-page
A creation recipe — a repeatable "build me one of these" pattern that bakes in the team's choices so every landing page lands in the same shape.
```markdown theme={"theme":"css-variables"}
# build-landing-page
Build a new marketing landing page from a brief. Before starting,
I'll give you a short brief — the offer, the primary call-to-action,
the audience, and any imagery direction. The flow:
1. **Confirm the brief.** Summarize what you understood back to me.
If anything is ambiguous (CTA target, audience, hero copy),
ask before scaffolding. Landing pages are cheap to rebuild but
annoying to partially redo.
2. **Add a page route** under `pages/marketing/` following the
project's slug convention. Wire it into the navigation entry
for marketing pages (not the main site nav).
3. **Lay out the page using existing blocks.** Start from
`HeroWithCTA`, then one or two supporting blocks from the
marketing block library (`FeatureGrid`, `TestimonialStrip`,
`SplitImageText`). Don't invent new blocks unless the brief
genuinely needs one — if it does, stop and ask.
4. **Wire the SEO composite.** Title, description, canonical URL,
OG image. Pull the OG image from the brief's hero imagery.
5. **Translate all user-facing strings** from the start — don't
leave them hardcoded "to translate later". Use the existing
key prefixes for the marketing namespace.
6. **Verify the page renders in both default and secondary
markets.** Request context resolution is where most landing
page bugs come from on this project.
7. **Give me a preview URL** and list what's left for me to review
(imagery review, copy check, analytics tags).
### Reminders
- Landing pages on this project all follow the hero + three-block
layout. If the brief asks for more, check in before adding.
- Analytics tagging goes through the `useMarketingEvent` composable
— don't wire events manually.
- Never inline images. Every image goes through the image CDN;
`` handles it.
```
Now when anyone types `/build-landing-page` for the spring sale campaign, the agent follows the same flow. Every landing page matches the rest of the site.
## Example: verify-accessibility
An audit command — a different shape from a build recipe. The agent reviews something that already exists and reports back.
```markdown theme={"theme":"css-variables"}
# verify-accessibility
Run an accessibility pass on the page, component, or feature I'm
pointing you at. Goal: surface issues that would fail our team's
a11y baseline, with enough context that I can fix them myself or
have you fix them.
1. **Identify the target.** If I gave you a route, audit that
route's rendered output. If I gave you a component, audit that
component's markup and any direct children it renders.
2. **Check the baselines** in this order (stop and report any
failures — don't keep digging if the first few are broken):
- Every interactive element has an accessible name
- Color contrast meets WCAG AA on brand colors (don't trust the
token defaults blindly; check the combinations in use on
this view)
- Keyboard-only navigation reaches every interactive element in
a sensible order, with visible focus indicators
- Form fields have visible labels, not only placeholders
- Images have alt text (empty `alt=""` for decorative is fine;
meaningful images need real text)
3. **Scan for our project-specific gotchas:**
- Custom dropdowns built from divs (should use `