Catalog API
Every Catalog action (25) and data model (9): endpoints, scopes, input, preconditions, events and examples.
Everything you sell or book — products, services, prices, categories and brands.
Module code catalog · 25 actions · 9 data models · 19 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: catalog (everything), catalog:read (read only) or catalog:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
brands | item_brand | /api/v1/brands | catalog.save_brand | ||
categories | item_category | /api/v1/categories | catalog.update_category | ||
product_components | item_component | /api/v1/product_components | |||
product_files | item_file | /api/v1/product_files | |||
product_option_values | item_option_value | /api/v1/product_option_values | |||
product_options | item_option | /api/v1/product_options | |||
products | item | /api/v1/products | catalog.create_item | catalog.update_item | |
variant_options | item_variant_option | /api/v1/variant_options | |||
variants | item_variant | /api/v1/variants | catalog.save_variant |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
catalog.activate | catalog:write | Offer it again |
catalog.assign_images | catalog:write | Assign image |
catalog.attach_file | catalog:write | Attach a picture or file |
catalog.bulk_update_items | catalog:write | Change several items |
catalog.create_category | catalog:manage | Add a category |
catalog.create_item | catalog:write | Add to the catalogue |
catalog.deactivate | catalog:delete | Stop offering it |
catalog.delete_items | catalog:delete | Delete items |
catalog.detach_file | catalog:write | Remove an attachment |
catalog.email_product_export | catalog:export | Email product export |
catalog.move_category | catalog:manage | Move a category |
catalog.publish | catalog:write | Publish |
catalog.remove_variant | catalog:delete | Remove a variant |
catalog.reorder_files | catalog:write | Reorder pictures |
catalog.save_brand | catalog:manage | Add or edit a brand |
catalog.save_variant | catalog:write | Add or edit a variant |
catalog.set_brand_visibility | catalog:manage | Show or hide a brand |
catalog.set_options | catalog:write | Set the variant axes |
catalog.set_package_contents | catalog:write | Set what a package contains |
catalog.set_price | catalog:write | Set a price |
catalog.unpublish | catalog:write | Unpublish |
catalog.update_category | catalog:manage | Edit a category |
catalog.update_file | catalog:write | Describe a picture |
catalog.update_item | catalog:write | Edit a catalogue entry |
catalog.update_item_metadata | catalog:write | Edit product metadata |
catalog.activate
Offer it again — Puts it back on the list.
Needs catalog:write · changes data
https://app.getera.io/api/v1/products/{id}/actions/activate- Canonical address:
POST /api/v1/actions/catalog.activate
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Item · id of item |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The entry must exist. |
already_active | It must be off the list. |
Events it publishes: catalog.activated
What it writes: item (update) — Marks it active
curl -X POST https://app.getera.io/api/v1/products/PRODUCT_ID/actions/activate \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"catalog.activated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.activate("PRODUCT_ID")era products activate PRODUCT_ID{
"name": "era__catalog__activate",
"arguments": {
"id": "PRODUCT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_active · 429 slow down
catalog.assign_images
Assign image — Appends the chosen image to every selected service or product, preserving their galleries.
Needs catalog:write · changes data
https://app.getera.io/api/v1/product_files/actions/assign_images- Canonical address:
POST /api/v1/actions/catalog.assign_images
Input
| Field | Type | Required | Notes |
|---|---|---|---|
ids | object / array | yes | Selected records |
fileId | uuid | yes | Image · id of file |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_items | Choose one to 500 records. |
file_not_found | The file must be accessible in this workspace. |
image_required | The file must be an image. |
not_found | Every selected record must be accessible. |
Events it publishes: catalog.files_changed
What it writes: item_file (insert) — Appends an image, keeping the existing gallery
curl -X POST https://app.getera.io/api/v1/product_files/actions/assign_images \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"ids":{},"fileId":"FILE_ID"}'{
"ok": true,
"result": "…",
"events": [
"catalog.files_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.productFiles.assignImages({
ids: {},
fileId: "FILE_ID"
})era product_files assign_images --data '{"ids":{},"fileId":"FILE_ID"}'{
"name": "era__catalog__assign_images",
"arguments": {
"ids": {},
"fileId": "FILE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_items, file_not_found, image_required, not_found · 429 slow down
catalog.attach_file
Attach a picture or file — A photo, a video, a datasheet or a download — on the item or on one variant.
Needs catalog:write · changes data
https://app.getera.io/api/v1/product_files/actions/attach_file- Canonical address:
POST /api/v1/actions/catalog.attach_file
Input
| Field | Type | Required | Notes |
|---|---|---|---|
itemId | uuid | yes | Item · id of item |
fileId | uuid | File · id of file | |
externalUrl | string | External URL · max 1000 | |
variantId | uuid | Variant · id of item_variant | |
role | string (enum) | Role · Allowed values: image, video, datasheet, download | |
alt | string | Alt text · max 250 | |
sortOrder | number | Order |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The item must exist. |
nothing_to_attach | A file or an external URL is needed. |
file_not_found | The file must exist. |
variant_not_on_item | The variant must belong to this item. |
Events it publishes: catalog.files_changed
What it writes: item_file (insert) — Attaches the file
curl -X POST https://app.getera.io/api/v1/product_files/actions/attach_file \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"itemId":"ITEM_ID"}'{
"ok": true,
"result": "…",
"events": [
"catalog.files_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.productFiles.attachFile({
itemId: "ITEM_ID"
})era product_files attach_file \
--item-id "ITEM_ID"{
"name": "era__catalog__attach_file",
"arguments": {
"itemId": "ITEM_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, nothing_to_attach, file_not_found, variant_not_on_item · 429 slow down
catalog.bulk_update_items
Change several items — Publish, unpublish, put back on or take off the list, or move to a category — from the list.
Needs catalog:write · changes data
https://app.getera.io/api/v1/products/actions/bulk_update_items- Canonical address:
POST /api/v1/actions/catalog.bulk_update_items
Input
| Field | Type | Required | Notes |
|---|---|---|---|
itemIds | object / array | yes | Items |
operation | string (enum) | yes | Operation · Allowed values: publish, unpublish, activate, deactivate, set_category |
categoryId | uuid | Category · id of item_category |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_items | At least one item is chosen. |
not_found | Every item must exist. |
category_not_found | The category must exist. |
Events it publishes: catalog.published, catalog.unpublished, catalog.activated, catalog.deactivated, catalog.item_updated
What it writes: item (update) — Writes the change on each item it applies to
curl -X POST https://app.getera.io/api/v1/products/actions/bulk_update_items \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"itemIds":{},"operation":"publish"}'{
"ok": true,
"result": "…",
"events": [
"catalog.published",
"catalog.unpublished",
"catalog.activated",
"catalog.deactivated",
"catalog.item_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.bulkUpdateItems({
itemIds: {},
operation: "publish"
})era products bulk_update_items --data '{"itemIds":{},"operation":"publish"}'{
"name": "era__catalog__bulk_update_items",
"arguments": {
"itemIds": {},
"operation": "publish"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_items, not_found, category_not_found · 429 slow down
catalog.create_category
Add a category — Groups the catalogue, on this screen and on the storefront.
Needs catalog:manage · changes data
https://app.getera.io/api/v1/categories/{id}/actions/create_category- Canonical address:
POST /api/v1/actions/catalog.create_category
Input
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Name · max 200 |
slug | string | Address · max 200 | |
description | string | Description · max 2000 | |
content | object / array | Page content | |
icon | string | Icon · max 64 | |
imageFileId | uuid | Image · id of file | |
parentId | uuid | Filled from the path {id} · Inside · id of item_category | |
isActive | boolean | Visible | |
sortOrder | number | Order | |
seoTitle | string | Search title · max 200 | |
seoDescription | string | Search description · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_name | The name must produce a usable address. |
slug_taken | The address must be free in this workspace. |
parent_not_found | The parent category must exist. |
Events it publishes: catalog.category_created
What it writes: item_category (insert) — Creates the category
curl -X POST https://app.getera.io/api/v1/categories/CATEGORY_ID/actions/create_category \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"catalog.category_created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.categories.createCategory("CATEGORY_ID", {
name: "Name"
})era categories create_category CATEGORY_ID \
--name "Name"{
"name": "era__catalog__create_category",
"arguments": {
"parentId": "CATEGORY_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_name, slug_taken, parent_not_found · 429 slow down
catalog.create_item
Add to the catalogue — Puts something new on the list of what can be sold or booked, with its first variant.
Needs catalog:write · changes data
https://app.getera.io/api/v1/products/actions/create_item- REST alias:
POST /api/v1/products - Canonical address:
POST /api/v1/actions/catalog.create_item
Input
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Name · max 200 |
slug | string | Address · max 200 | |
kind | string (enum) | Kind · A service has time; a product ships. A package is several of either. · Allowed values: service, product, package, gift_card | |
fulfilment | string (enum) | Delivered as · Allowed values: appointment, home_service, group_class, shipping, digital | |
categoryId | uuid | Category · id of item_category | |
brandId | uuid | Brand · id of item_brand | |
description | string | Description · max 4000 | |
summary | string | Short description · max 500 | |
content | object / array | Page content | |
sortOrder | number | Order | |
model | string | Model · max 200 | |
sku | string | SKU · max 100 | |
priceAmount | number (money) | Price (minor units) · Leave empty for “ask us” — 55.4% of the live catalogue is priced that way. · min 0 | |
durationMinutes | number | Duration (minutes) · min 5 · max 1440 | |
capacity | number | Capacity · Above 1 makes it a class. · min 1 | |
inShop | boolean | Put it in the shop · Published at once and priced in the shop currency. No shop → store_missing; then retail.set_item_listing (no channel) lists it. | |
imageFileId | uuid | Picture · An uploaded file — e.g. the photo attached in chat. · id of file |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_name | The name must produce a usable address. |
store_missing | Put in the shop, the workspace must have an open shop (retail.open_shop). |
file_not_found | The picture must be a file in this workspace. |
slug_taken | The address must be free in this workspace. |
fulfilment_not_allowed_for_kind | A product does not take appointments. |
duration_on_product | A product has no duration. |
category_not_found | The category must exist. |
brand_not_found | The brand must exist. |
Events it publishes: catalog.item_created, catalog.files_changed, catalog.published
What it writes: item (insert) — Creates the catalogue entry; item_variant (insert) — Creates its default variant; item_file (insert) — Attaches the picture, when one is given
curl -X POST https://app.getera.io/api/v1/products/actions/create_item \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"catalog.item_created",
"catalog.files_changed",
"catalog.published"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.create({
name: "Name"
})era products create \
--name "Name"{
"name": "era__catalog__create_item",
"arguments": {
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_name, store_missing, file_not_found, slug_taken, fulfilment_not_allowed_for_kind, duration_on_product, category_not_found, brand_not_found · 429 slow down
catalog.deactivate
Stop offering it — Takes it off the list, naming any appointments already booked for it.
Needs catalog:delete · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/products/{id}/actions/deactivate- Canonical address:
POST /api/v1/actions/catalog.deactivate
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Item · id of item |
force | boolean | Deactivate anyway |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The entry must exist. |
already_inactive | It must still be on the list. |
has_upcoming | Booked appointments are named before it is taken off. |
Events it publishes: catalog.deactivated
What it writes: item (update) — Marks it inactive
curl -X POST https://app.getera.io/api/v1/products/PRODUCT_ID/actions/deactivate \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"force":true}'{
"ok": true,
"result": "…",
"events": [
"catalog.deactivated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.deactivate("PRODUCT_ID", {
force: true
})era products deactivate PRODUCT_ID \
--force true{
"name": "era__catalog__deactivate",
"arguments": {
"id": "PRODUCT_ID",
"force": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:delete · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_inactive, has_upcoming · 429 slow down
catalog.delete_items
Delete items — Removes them for good. Orders keep their frozen names; bookings and packages refuse it.
Needs catalog:delete · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/products/actions/delete_items- Canonical address:
POST /api/v1/actions/catalog.delete_items
Input
| Field | Type | Required | Notes |
|---|---|---|---|
itemIds | object / array | yes | Items |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_items | At least one item is chosen. |
not_found | Every item must exist. |
item_in_use | Nothing a booking, package or class points at is deleted. |
Events it publishes: catalog.item_deleted
What it writes: item (delete) — Removes the items and their variants
curl -X POST https://app.getera.io/api/v1/products/actions/delete_items \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"itemIds":{}}'{
"ok": true,
"result": "…",
"events": [
"catalog.item_deleted"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.deleteItems({
itemIds: {}
})era products delete_items --data '{"itemIds":{}}'{
"name": "era__catalog__delete_items",
"arguments": {
"itemIds": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:delete · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_items, not_found, item_in_use · 429 slow down
catalog.detach_file
Remove an attachment — Queues it for removal, as the source does.
Needs catalog:write · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/product_files/{id}/actions/detach_file- Canonical address:
POST /api/v1/actions/catalog.detach_file
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Attachment · id of item_file |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
attachment_not_found | The attachment must exist and still be attached. |
Events it publishes: catalog.files_changed
What it writes: item_file (update) — Marks it for removal
curl -X POST https://app.getera.io/api/v1/product_files/PRODUCT_FILE_ID/actions/detach_file \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"catalog.files_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.productFiles.detachFile("PRODUCT_FILE_ID")era product_files detach_file PRODUCT_FILE_ID{
"name": "era__catalog__detach_file",
"arguments": {
"id": "PRODUCT_FILE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 attachment_not_found · 429 slow down
catalog.email_product_export
Email product export — Builds the selected XLSX in the job worker and emails it as an attachment.
Needs catalog:export · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/actions/catalog.email_product_exportInput
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Recipient · max 320 |
fields | object / array | yes | Fields |
filter | object / array | Current filters |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_export_email | The recipient must be a usable email address. |
invalid_export_fields | At least one known product export field must be selected. |
Events it publishes: catalog.export_queued
curl -X POST https://app.getera.io/api/v1/actions/catalog.email_product_export \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"email":"Recipient","fields":{}}'{
"ok": true,
"result": "…",
"events": [
"catalog.export_queued"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.actions.run("catalog.email_product_export", {
email: "Recipient",
fields: {}
})era catalog email_product_export --data '{"email":"Recipient","fields":{}}'{
"name": "era__catalog__email_product_export",
"arguments": {
"email": "Recipient",
"fields": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:export · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_export_email, invalid_export_fields · 429 slow down
catalog.move_category
Move a category — Puts a category inside another one, or back at the root.
Needs catalog:manage · changes data
https://app.getera.io/api/v1/categories/{id}/actions/move_category- Canonical address:
POST /api/v1/actions/catalog.move_category
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Category · id of item_category |
parentId | uuid | Inside · id of item_category |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The category must exist. |
parent_not_found | The parent category must exist. |
cycle | A category cannot be moved inside itself. |
Events it publishes: catalog.category_moved
What it writes: item_category (update) — Moves the category
curl -X POST https://app.getera.io/api/v1/categories/CATEGORY_ID/actions/move_category \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"parentId":"ITEM_CATEGORY_ID"}'{
"ok": true,
"result": "…",
"events": [
"catalog.category_moved"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.categories.moveCategory("CATEGORY_ID", {
parentId: "ITEM_CATEGORY_ID"
})era categories move_category CATEGORY_ID \
--parent-id "ITEM_CATEGORY_ID"{
"name": "era__catalog__move_category",
"arguments": {
"id": "CATEGORY_ID",
"parentId": "ITEM_CATEGORY_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, parent_not_found, cycle · 429 slow down
catalog.publish
Publish — Puts it on the storefront.
Needs catalog:write · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/products/{id}/actions/publish- Canonical address:
POST /api/v1/actions/catalog.publish
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Item · id of item |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The entry must exist. |
already_published | It must still be a draft. |
no_variant | There must be something to sell. |
no_price | A gift card needs a value. |
Events it publishes: catalog.published
What it writes: item (update) — Stamps published_at
curl -X POST https://app.getera.io/api/v1/products/PRODUCT_ID/actions/publish \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"catalog.published"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.publish("PRODUCT_ID")era products publish PRODUCT_ID{
"name": "era__catalog__publish",
"arguments": {
"id": "PRODUCT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_published, no_variant, no_price · 429 slow down
catalog.remove_variant
Remove a variant — Drops one variant, unless it is the last or something already points at it.
Needs catalog:delete · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/variants/{id}/actions/remove_variant- Canonical address:
POST /api/v1/actions/catalog.remove_variant
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Variant · id of item_variant |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
variant_not_found | The variant must exist. |
last_variant | An item always keeps at least one variant. |
variant_in_use | Bookings and packages pointing at it are named first. |
Events it publishes: catalog.variant_removed
What it writes: item_variant (delete) — Removes the variant
curl -X POST https://app.getera.io/api/v1/variants/VARIANT_ID/actions/remove_variant \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"catalog.variant_removed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.variants.removeVariant("VARIANT_ID")era variants remove_variant VARIANT_ID{
"name": "era__catalog__remove_variant",
"arguments": {
"id": "VARIANT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:delete · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 variant_not_found, last_variant, variant_in_use · 429 slow down
catalog.reorder_files
Reorder pictures — The order the storefront shows them in.
Needs catalog:write · changes data
https://app.getera.io/api/v1/product_files/actions/reorder_files- Canonical address:
POST /api/v1/actions/catalog.reorder_files
Input
| Field | Type | Required | Notes |
|---|---|---|---|
itemId | uuid | yes | Item · id of item |
attachmentIds | object / array | yes | Attachments |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The item must exist. |
attachment_not_on_item | Every attachment must be this item’s. |
Events it publishes: catalog.files_changed
What it writes: item_file (update) — Writes the order
curl -X POST https://app.getera.io/api/v1/product_files/actions/reorder_files \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"itemId":"ITEM_ID","attachmentIds":{}}'{
"ok": true,
"result": "…",
"events": [
"catalog.files_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.productFiles.reorderFiles({
itemId: "ITEM_ID",
attachmentIds: {}
})era product_files reorder_files --data '{"itemId":"ITEM_ID","attachmentIds":{}}'{
"name": "era__catalog__reorder_files",
"arguments": {
"itemId": "ITEM_ID",
"attachmentIds": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, attachment_not_on_item · 429 slow down
catalog.save_brand
Add or edit a brand — Whose product this is — with its own page, site and logo.
Needs catalog:manage · changes data
https://app.getera.io/api/v1/brands/{id}/actions/save_brand- REST alias:
PATCH /api/v1/brands/{id} - Canonical address:
POST /api/v1/actions/catalog.save_brand
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Brand · id of item_brand | |
name | string | yes | Name · max 200 |
slug | string | Address · max 200 | |
description | string | Description · max 2000 | |
websiteUrl | string | Website · max 500 | |
parentId | uuid | Part of · id of item_brand | |
isFeatured | boolean | Show in the showcase | |
isActive | boolean | Visible | |
sortOrder | number | Order | |
imageFileId | uuid | Logo · id of file |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_name | The name must produce a usable address. |
slug_taken | The address must be free in this workspace. |
parent_not_found | The parent brand must exist. |
cycle | A brand cannot be its own parent. |
not_found | The brand must exist when editing. |
Events it publishes: catalog.brand_created, catalog.brand_updated
What it writes: item_brand (upsert) — Writes the brand
curl -X POST https://app.getera.io/api/v1/brands/BRAND_ID/actions/save_brand \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"catalog.brand_created",
"catalog.brand_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.brands.saveBrand("BRAND_ID", {
name: "Name"
})era brands save_brand BRAND_ID \
--name "Name"{
"name": "era__catalog__save_brand",
"arguments": {
"id": "BRAND_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_name, slug_taken, parent_not_found, cycle, not_found · 429 slow down
catalog.save_variant
Add or edit a variant — The thing actually sold: its SKU, price, duration, capacity and axes.
Needs catalog:write · changes data
https://app.getera.io/api/v1/variants/{id}/actions/save_variant- REST alias:
PATCH /api/v1/variants/{id} - Canonical address:
POST /api/v1/actions/catalog.save_variant
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Variant · id of item_variant | |
itemId | uuid | Item · id of item | |
name | string | Variant name · max 200 | |
sku | string | SKU · max 100 | |
priceAmount | number (money) | Price (minor units) · min 0 | |
costPriceAmount | number (money) | Cost (minor units) · min 0 | |
durationMinutes | number | Duration (minutes) · min 5 · max 1440 | |
capacity | number | Capacity · min 1 | |
weightGrams | number | Weight (g) · min 0 | |
quantityLimitPerCustomer | number | Limit per customer · min 1 | |
trackInventory | boolean | Track stock | |
sortOrder | number | Order | |
optionValueIds | string | Option values · Comma-separated option value ids — one per axis. · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The item must exist. |
variant_not_found | The variant must exist. |
sku_taken | A SKU belongs to one variant. |
weight_on_unshipped | Only something that ships has a weight. |
duration_on_product | A product has no duration. |
option_value_not_on_item | Every axis value must belong to this item. |
variant_combination_exists | Two variants may not share a combination. |
Events it publishes: catalog.variant_changed
What it writes: item_variant (upsert) — Writes the variant; item_variant_option (upsert) — Writes which axes it sits on
curl -X POST https://app.getera.io/api/v1/variants/VARIANT_ID/actions/save_variant \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"itemId":"ITEM_ID"}'{
"ok": true,
"result": "…",
"events": [
"catalog.variant_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.variants.saveVariant("VARIANT_ID", {
itemId: "ITEM_ID"
})era variants save_variant VARIANT_ID \
--item-id "ITEM_ID"{
"name": "era__catalog__save_variant",
"arguments": {
"id": "VARIANT_ID",
"itemId": "ITEM_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, variant_not_found, sku_taken, weight_on_unshipped, duration_on_product, option_value_not_on_item, variant_combination_exists · 429 slow down
catalog.set_brand_visibility
Show or hide a brand — Keeps product history intact while controlling whether the brand appears in pickers and showcases.
Needs catalog:manage · changes data
https://app.getera.io/api/v1/brands/{id}/actions/set_brand_visibility- Canonical address:
POST /api/v1/actions/catalog.set_brand_visibility
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Brand · id of item_brand |
isActive | boolean | yes | Visible |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The brand must exist. |
Events it publishes: catalog.brand_updated
What it writes: item_brand (update) — Changes storefront visibility
curl -X POST https://app.getera.io/api/v1/brands/BRAND_ID/actions/set_brand_visibility \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"isActive":true}'{
"ok": true,
"result": "…",
"events": [
"catalog.brand_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.brands.setBrandVisibility("BRAND_ID", {
isActive: true
})era brands set_brand_visibility BRAND_ID \
--is-active true{
"name": "era__catalog__set_brand_visibility",
"arguments": {
"id": "BRAND_ID",
"isActive": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found · 429 slow down
catalog.set_options
Set the variant axes — What tells one variant from another — size, colour, length.
Needs catalog:write · changes data
https://app.getera.io/api/v1/product_options/actions/set_options- Canonical address:
POST /api/v1/actions/catalog.set_options
Input
| Field | Type | Required | Notes |
|---|---|---|---|
itemId | uuid | yes | Item · id of item |
options | object / array | yes | Options · [{ "name": "Size", "values": [{ "label": "S" }, { "label": "M" }] }] |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The item must exist. |
invalid_options | Every option needs a name and labelled values. |
option_in_use_by_variant | An axis a variant sits on cannot vanish. |
Events it publishes: catalog.options_changed
What it writes: item_option (upsert) — Replaces the axes; item_option_value (upsert) — Replaces their values
curl -X POST https://app.getera.io/api/v1/product_options/actions/set_options \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"itemId":"ITEM_ID","options":{}}'{
"ok": true,
"result": "…",
"events": [
"catalog.options_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.productOptions.setOptions({
itemId: "ITEM_ID",
options: {}
})era product_options set_options --data '{"itemId":"ITEM_ID","options":{}}'{
"name": "era__catalog__set_options",
"arguments": {
"itemId": "ITEM_ID",
"options": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, invalid_options, option_in_use_by_variant · 429 slow down
catalog.set_package_contents
Set what a package contains — Replaces a package’s contents — variants, with quantities, in order.
Needs catalog:write · changes data
https://app.getera.io/api/v1/product_components/actions/set_package_contents- Canonical address:
POST /api/v1/actions/catalog.set_package_contents
Input
| Field | Type | Required | Notes |
|---|---|---|---|
packageItemId | uuid | yes | Package · id of item |
contents | string | yes | Contents · Comma-separated variant ids, optionally id:quantity — e.g. “…:10” for ten sessions. · max 4000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The package must exist. |
not_a_package | The entry must be of kind “package”. |
package_empty | A package needs at least one line. |
invalid_quantity | A quantity is a whole number, one or more. |
component_not_found | Every line must point at a real variant. |
self_component | A package cannot contain itself. |
component_is_package | Packages do not nest. |
Events it publishes: catalog.package_changed
What it writes: item_component (upsert) — Replaces the package contents
curl -X POST https://app.getera.io/api/v1/product_components/actions/set_package_contents \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"packageItemId":"ITEM_ID","contents":"Contents"}'{
"ok": true,
"result": "…",
"events": [
"catalog.package_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.productComponents.setPackageContents({
packageItemId: "ITEM_ID",
contents: "Contents"
})era product_components set_package_contents \
--package-item-id "ITEM_ID" \
--contents "Contents"{
"name": "era__catalog__set_package_contents",
"arguments": {
"packageItemId": "ITEM_ID",
"contents": "Contents"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, not_a_package, package_empty, invalid_quantity, component_not_found, self_component, component_is_package · 429 slow down
catalog.set_price
Set a price — The list price of one variant. Signed agreements do not move with it.
Needs catalog:write · changes data
https://app.getera.io/api/v1/variants/{id}/actions/set_price- Canonical address:
POST /api/v1/actions/catalog.set_price
Input
| Field | Type | Required | Notes |
|---|---|---|---|
variantId | uuid | yes | Filled from the path {id} · Variant · id of item_variant |
priceAmount | number (money) | Price (minor units) · min 0 | |
currency | string | Currency · max 3 | |
costPriceAmount | number (money) | Cost (minor units) · min 0 | |
priorPriceAmount | number (money) | Previous price · EU Omnibus. · min 0 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
variant_not_found | The variant must exist. |
currency_mismatch | One catalogue, one currency. |
Events it publishes: catalog.price_changed
What it writes: item_variant (update) — Writes the price
curl -X POST https://app.getera.io/api/v1/variants/VARIANT_ID/actions/set_price \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"priceAmount":1}'{
"ok": true,
"result": "…",
"events": [
"catalog.price_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.variants.setPrice("VARIANT_ID", {
priceAmount: 1
})era variants set_price VARIANT_ID \
--price-amount 1{
"name": "era__catalog__set_price",
"arguments": {
"variantId": "VARIANT_ID",
"priceAmount": 1
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 variant_not_found, currency_mismatch · 429 slow down
catalog.unpublish
Unpublish — Takes it back to draft without taking it off the internal list.
Needs catalog:write · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/products/{id}/actions/unpublish- Canonical address:
POST /api/v1/actions/catalog.unpublish
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Item · id of item |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The entry must exist. |
not_published | It must be published. |
Events it publishes: catalog.unpublished
What it writes: item (update) — Clears published_at
curl -X POST https://app.getera.io/api/v1/products/PRODUCT_ID/actions/unpublish \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"catalog.unpublished"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.unpublish("PRODUCT_ID")era products unpublish PRODUCT_ID{
"name": "era__catalog__unpublish",
"arguments": {
"id": "PRODUCT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, not_published · 429 slow down
catalog.update_category
Edit a category — Updates the category copy, address, content and storefront visibility.
Needs catalog:manage · changes data
https://app.getera.io/api/v1/categories/{id}/actions/update_category- REST alias:
PATCH /api/v1/categories/{id} - Canonical address:
POST /api/v1/actions/catalog.update_category
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Category · id of item_category |
name | string | Name · max 200 | |
slug | string | Address · max 200 | |
description | string | Description · max 2000 | |
content | object / array | Page content | |
icon | string | Icon · max 64 | |
imageFileId | uuid | Image · id of file | |
isActive | boolean | Visible | |
sortOrder | number | Order | |
seoTitle | string | Search title · max 200 | |
seoDescription | string | Search description · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The category must exist. |
invalid_name | The name must produce a usable address. |
slug_taken | The address must be free in this workspace. |
Events it publishes: catalog.category_updated
What it writes: item_category (update) — Updates the category
curl -X POST https://app.getera.io/api/v1/categories/CATEGORY_ID/actions/update_category \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"catalog.category_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.categories.updateCategory("CATEGORY_ID", {
name: "Name"
})era categories update_category CATEGORY_ID \
--name "Name"{
"name": "era__catalog__update_category",
"arguments": {
"id": "CATEGORY_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, invalid_name, slug_taken · 429 slow down
catalog.update_file
Describe a picture — Its alt text, and which variant it shows.
Needs catalog:write · changes data
https://app.getera.io/api/v1/product_files/{id}/actions/update_file- Canonical address:
POST /api/v1/actions/catalog.update_file
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Attachment · id of item_file |
alt | string | Alt text · max 250 | |
variantId | uuid | Variant · id of item_variant |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
attachment_not_found | The attachment must exist and still be attached. |
variant_not_on_item | The variant must belong to the same item. |
Events it publishes: catalog.files_changed
What it writes: item_file (update) — Writes the alt text and variant
curl -X POST https://app.getera.io/api/v1/product_files/PRODUCT_FILE_ID/actions/update_file \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"alt":"Alt text"}'{
"ok": true,
"result": "…",
"events": [
"catalog.files_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.productFiles.updateFile("PRODUCT_FILE_ID", {
alt: "Alt text"
})era product_files update_file PRODUCT_FILE_ID \
--alt "Alt text"{
"name": "era__catalog__update_file",
"arguments": {
"id": "PRODUCT_FILE_ID",
"alt": "Alt text"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 attachment_not_found, variant_not_on_item · 429 slow down
catalog.update_item
Edit a catalogue entry — Its name, address, grouping, copy and SEO.
Needs catalog:write · changes data
https://app.getera.io/api/v1/products/{id}/actions/update_item- REST alias:
PATCH /api/v1/products/{id} - Canonical address:
POST /api/v1/actions/catalog.update_item
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Item · id of item |
name | string | Name · max 200 | |
slug | string | Address · max 200 | |
categoryId | uuid | Category · id of item_category | |
brandId | uuid | Brand · id of item_brand | |
description | string | Description · max 4000 | |
summary | string | Short description · max 500 | |
content | object / array | Page content | |
model | string | Model · max 200 | |
seoTitle | string | SEO title · max 200 | |
seoDescription | string | SEO description · max 500 | |
isFeatured | boolean | Featured | |
fulfilment | string (enum) | Delivered as · Allowed values: appointment, home_service, group_class, shipping, digital | |
externalReference | string | External key · max 200 | |
sortOrder | number | Order | |
features | object / array | Features · A list of short lines shown as bullet points. |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The entry must exist. |
invalid_name | The name must produce a usable address. |
slug_taken | The address must be free in this workspace. |
fulfilment_not_allowed_for_kind | A product does not take appointments. |
category_not_found | The category must exist. |
brand_not_found | The brand must exist. |
Events it publishes: catalog.item_updated
What it writes: item (update) — Updates the catalogue entry
curl -X POST https://app.getera.io/api/v1/products/PRODUCT_ID/actions/update_item \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"catalog.item_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.updateItem("PRODUCT_ID", {
name: "Name"
})era products update_item PRODUCT_ID \
--name "Name"{
"name": "era__catalog__update_item",
"arguments": {
"id": "PRODUCT_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, invalid_name, slug_taken, fulfilment_not_allowed_for_kind, category_not_found, brand_not_found · 429 slow down
catalog.update_item_metadata
Edit product metadata — Replaces the entry’s key → value metadata, public or private.
Needs catalog:write · changes data
https://app.getera.io/api/v1/products/{id}/actions/update_item_metadata- Canonical address:
POST /api/v1/actions/catalog.update_item_metadata
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Item · id of item |
metadata | object / array | Metadata | |
privateMetadata | object / array | Private metadata |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The entry must exist. |
Events it publishes: catalog.item_updated
What it writes: item (update) — Writes the metadata
curl -X POST https://app.getera.io/api/v1/products/PRODUCT_ID/actions/update_item_metadata \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"metadata":{}}'{
"ok": true,
"result": "…",
"events": [
"catalog.item_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.products.updateItemMetadata("PRODUCT_ID", {
metadata: {}
})era products update_item_metadata PRODUCT_ID --data '{"metadata":{}}'{
"name": "era__catalog__update_item_metadata",
"arguments": {
"id": "PRODUCT_ID",
"metadata": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold catalog:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found · 429 slow down
Data models
ItemBrand
Table item_brand · Resource brands
GET /api/v1/brandsGET /api/v1/brands/{id}PATCH /api/v1/brands/{id}→catalog.save_brand
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
slug | string · text | |||
description | string · text | yes | ||
website_url | string · text | yes | ||
image_file_id | string (uuid) · uuid | yes | file | |
parent_id | string (uuid) · uuid | yes | item_brand | |
is_active | boolean · bool | |||
is_featured | boolean · bool | |||
sort_order | integer · int4 | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Unique: workspace_id, slug
ItemCategory
Table item_category · Resource categories
GET /api/v1/categoriesGET /api/v1/categories/{id}PATCH /api/v1/categories/{id}→catalog.update_category
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
slug | string · text | |||
description | string · text | yes | ||
icon | string · text | yes | ||
image_file_id | string (uuid) · uuid | yes | file | |
content | object · jsonb | yes | ||
sort_order | integer · int4 | |||
is_active | boolean · bool | |||
parent_id | string (uuid) · uuid | yes | item_category | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
seo_title | string · text | yes | ||
seo_description | string · text | yes |
Primary key: id
Unique: workspace_id, slug
ItemComponent
Table item_component · Resource product_components
GET /api/v1/product_componentsGET /api/v1/product_components/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
parent_item_id | string (uuid) · uuid | item | ||
sort_order | integer · int4 | |||
item_variant_id | string (uuid) · uuid | item_variant | ||
quantity | integer · int4 |
Primary key: id
Unique: parent_item_id, item_variant_id
Checks:
CHECK ((quantity >= 1))
ItemFile
Table item_file · Resource product_files
GET /api/v1/product_filesGET /api/v1/product_files/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
item_id | string (uuid) · uuid | item | ||
variant_id | string (uuid) · uuid | yes | item_variant | |
file_id | string (uuid) · uuid | yes | file | |
role | string · text | image, video, datasheet, download | ||
alt | string · text | yes | ||
external_url | string · text | yes | ||
to_remove | boolean · bool | |||
sort_order | integer · int4 | |||
created_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK (((file_id IS NOT NULL) OR (external_url IS NOT NULL)))
ItemOptionValue
Table item_option_value · Resource product_option_values
GET /api/v1/product_option_valuesGET /api/v1/product_option_values/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
option_id | string (uuid) · uuid | item_option | ||
label | string · text | |||
swatch | string · text | yes | ||
file_id | string (uuid) · uuid | yes | file | |
sort_order | integer · int4 |
Primary key: id
Unique: option_id, label
ItemOption
Table item_option · Resource product_options
GET /api/v1/product_optionsGET /api/v1/product_options/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
item_id | string (uuid) · uuid | item | ||
name | string · text | |||
sort_order | integer · int4 |
Primary key: id
Unique: item_id, name
Item
Table item · Resource products
GET /api/v1/productsGET /api/v1/products/{id}POST /api/v1/products→catalog.create_itemPATCH /api/v1/products/{id}→catalog.update_item
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
category_id | string (uuid) · uuid | yes | item_category | |
name | string · text | |||
slug | string · text | |||
description | string · text | yes | ||
features | string[] · text[] | yes | ||
fulfilment | string · text | appointment, home_service, group_class, shipping, digital | ||
is_active | boolean · bool | |||
sort_order | integer · int4 | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
kind | string · text | product, service, package, gift_card | ||
summary | string · text | yes | ||
content | object · jsonb | yes | ||
seo_title | string · text | yes | ||
seo_description | string · text | yes | ||
published_at | string (date-time) · timestamptz | yes | ||
is_featured | boolean · bool | |||
model | string · text | yes | ||
rating | number · float4 | yes | ||
tax_class_id | string (uuid) · uuid | yes | ||
external_reference | string · text | yes | ||
brand_id | string (uuid) · uuid | yes | item_brand | |
default_variant_id | string (uuid) · uuid | yes | item_variant | |
is_public | boolean · bool | yes | ||
metadata | object · jsonb | |||
private_metadata | object · jsonb |
Primary key: id
Unique: workspace_id, slug
Checks:
CHECK ( CASE kind WHEN 'service'::text THEN (fulfilment = ANY (ARRAY['appointment'::text, 'home_service'::text, 'group_class'::text])) WHEN 'product'::text THEN (fulfilment = ANY (ARRAY['shipping'::text, 'digital'::text])) ELSE true END)
ItemVariantOption
Table item_variant_option · Resource variant_options
GET /api/v1/variant_optionsGET /api/v1/variant_options/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
variant_id | string (uuid) · uuid | item_variant | ||
option_id | string (uuid) · uuid | item_option | ||
option_value_id | string (uuid) · uuid | item_option_value |
Primary key: id
Unique: variant_id, option_id
ItemVariant
Table item_variant · Resource variants
GET /api/v1/variantsGET /api/v1/variants/{id}PATCH /api/v1/variants/{id}→catalog.save_variant
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
item_id | string (uuid) · uuid | item | ||
name | string · text | |||
sku | string · text | yes | ||
price_amount | string (int64) · int8 | yes | ||
price_currency | string · bpchar | yes | max 3 | |
cost_price_amount | string (int64) · int8 | yes | ||
prior_price_amount | string (int64) · int8 | yes | ||
duration_minutes | integer · int4 | yes | ||
capacity | integer · int4 | |||
weight_grams | string (int64) · int8 | yes | ||
quantity_limit_per_customer | integer · int4 | yes | ||
track_inventory | boolean · bool | |||
external_reference | string · text | yes | ||
is_active | boolean · bool | |||
sort_order | integer · int4 | |||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((capacity >= 1))CHECK (((cost_price_amount IS NULL) OR (cost_price_amount >= 0)))CHECK (((duration_minutes IS NULL) OR ((duration_minutes >= 5) AND (duration_minutes <= 1440))))CHECK (((price_amount IS NULL) OR (price_amount >= 0)))CHECK (((quantity_limit_per_customer IS NULL) OR (quantity_limit_per_customer >= 1)))CHECK (((weight_grams IS NULL) OR (weight_grams >= 0)))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
catalog.activated | Something is on sale again. | id |
catalog.brand_created | A brand was added. | id, slug |
catalog.brand_updated | A brand was edited. | id, slug |
catalog.category_created | A category was added. | id, name, slug |
catalog.category_moved | A category changed parent. | id, parentId |
catalog.category_updated | A category was edited. | id, slug |
catalog.deactivated | Something stopped being sold. | id, keptAppointments |
catalog.export_queued | A product XLSX was queued for email delivery. | jobId, email, fields |
catalog.files_changed | A picture or file was attached or removed. | id, attachmentId |
catalog.item_created | Something was added to the catalogue. | id, name, slug, kind, variantId |
catalog.item_deleted | A catalogue entry was deleted. | id |
catalog.item_updated | A catalogue entry was edited. | id, slug |
catalog.options_changed | The variant axes changed. | id, count |
catalog.package_changed | A package’s contents were replaced. | id, count |
catalog.price_changed | A list price changed. | variantId, itemId, amount, currency |
catalog.published | Something went on the storefront. | id, slug |
catalog.unpublished | Something went back to draft. | id |
catalog.variant_changed | A variant was added or edited. | id, itemId |
catalog.variant_removed | A variant was removed. | id, itemId |