Reference: API-02 API reference
SaveLayer Online Store endpoints, request and response payloads, and examples. For every error code and HTTP
status, see
Error codes
arrow_forward
.
01 / BASICS
Base URL and auth
Merchants configure the Shopify app proxy subpath. Storefront requests hit https://{shop}/apps/{proxy-subpath}/… with Shopify-signed query parameters (shop, signature, timestamp, etc.). Do not build unsigned proxy URLs by hand—use Liquid, the theme app extension, or the SDK.
SaveLayer handlers are mounted under the app path /proxy/api/*. The paths below are the suffix after your proxy root (e.g. /save, /list).
All mutating routes require an installed app and a logged-in customer. Failed proxy verification yields INVALID_PROXY_SIGNATURE; a valid proxy request without a logged-in customer yields AUTH_REQUIRED (401)—not CUSTOMER_REQUIRED (see Error codes arrow_forward).
02 / ENVELOPES
Success and error JSON
Success responses use ProxySuccessEnvelopeSchema: ok: true and a data object shaped per endpoint.
application/json 200
{
"ok": true,
"data": {
"id": "gid://shopify/Metaobject/456",
"state": "active"
}
}
Errors use ProxyErrorEnvelopeSchema: ok: false and an error object. HTTP status follows ProxyErrorStatusByCode (for example 401 AUTH_REQUIRED, 409 DUPLICATE_SAVE, 429 RATE_LIMITED, 402 PLAN_LIMIT_REACHED). Full code list: Error codes arrow_forward.
The error object matches ProxyErrorSchema: code (a ProxyErrorCode string), message (human-readable), and retryable (boolean). Wrong HTTP method (for example POST to /list) returns VALIDATION_ERROR with status 405.
application/json 400
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid save payload.",
"retryable": false
}
}
03 / TYPES
EntityTypeSchema
Allowed entityType values:
-
product -
variant -
collection -
metaobject -
article -
page -
configuration
04 / ENDPOINTS
Operations
Endpoints (path suffix after your app proxy root, e.g. /apps/savelayer): POST /save, POST /remove, POST /toggle, POST /batch, GET /list, GET /is-saved. Mutating JSON routes expect Content-Type: application/json. /list and /is-saved use query parameters only (no body). Shopify app proxy query parameters (shop, signature, timestamp, logged_in_customer_id, etc.) are sent in addition to the fields below.
POST /save
SaveRequestSchema Summary Creates or activates a save_item for the list context (creates save_list if needed). POST only. Common proxy failures (all endpoints): INVALID_PROXY_SIGNATURE (401), AUTH_REQUIRED (401) without logged-in customer, SHOP_NOT_INSTALLED (403).
Request body (
SaveRequestSchema)
| Field | Type | Required | Notes |
context | string | yes | Non-empty list key (e.g. wishlist handle). |
entityType | EntityTypeSchema | yes | One of: product, variant, collection, metaobject, article, page, configuration. |
entityGid | string | yes | Shopify GID of the entity. |
source | string | yes | Provenance (e.g. theme-app-extension). |
Success data (
SaveOperationResultSchema)
| Field | Type | Required | Notes |
id | string | yes | save_item metaobject GID. |
state | "active" | "removed" | yes | Resulting state. |
Common error codes
-
DUPLICATE_SAVE (409) — item already active -
VALIDATION_ERROR (400) — body fails SaveRequestSchema or invalid JSON -
SHOPIFY_UPSTREAM_ERROR (502), INTERNAL_ERROR (500)
Plan / limits
No endpoint-specific plan tier. Monthly budgets and batch rules: see limits section.
Example request
curl
curl -sS -X POST 'https://{shop}/apps/{proxy-subpath}/save?shop=...&signature=...×tamp=...&logged_in_customer_id=...' \
-H 'Content-Type: application/json' \
-d '{"context":"wishlist","entityType":"product","entityGid":"gid://shopify/Product/123","source":"theme"}'
fetch
await fetch("/apps/{proxy-subpath}/save?shop=...&signature=...×tamp=...&logged_in_customer_id=...", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
context: "wishlist",
entityType: "product",
entityGid: "gid://shopify/Product/123",
source: "theme",
}),
});
Example response
application/json 200
{
"ok": true,
"data": {
"id": "gid://shopify/Metaobject/456",
"state": "active"
}
}
Example error response
application/json 409
{
"ok": false,
"error": {
"code": "DUPLICATE_SAVE",
"message": "The item is already saved.",
"retryable": false
}
}
POST /remove
RemoveRequestSchema Summary Soft-removes the save_item (status removed). If the list does not exist or the item is missing, returns ITEM_NOT_FOUND (404). Idempotent when already removed: returns 200 with state removed.
Request body (
RemoveRequestSchema)
| Field | Type | Required | Notes |
context | string | yes | — |
entityType | EntityTypeSchema | yes | One of: product, variant, collection, metaobject, article, page, configuration. |
entityGid | string | yes | — |
Success data (
SaveOperationResultSchema)
| Field | Type | Required | Notes |
id | string | yes | — |
state | "active" | "removed" | yes | — |
Common error codes
-
ITEM_NOT_FOUND (404) — no save_list or no matching save_item -
VALIDATION_ERROR (400) — body fails RemoveRequestSchema
Plan / limits
No endpoint-specific plan tier. Monthly budgets and batch rules: see limits section.
Example request
curl
curl -sS -X POST 'https://{shop}/apps/{proxy-subpath}/remove?shop=...&signature=...×tamp=...&logged_in_customer_id=...' \
-H 'Content-Type: application/json' \
-d '{"context":"wishlist","entityType":"product","entityGid":"gid://shopify/Product/123"}'
fetch
await fetch("/apps/{proxy-subpath}/remove?shop=...&signature=...×tamp=...&logged_in_customer_id=...", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
context: "wishlist",
entityType: "product",
entityGid: "gid://shopify/Product/123",
}),
});
Example response
application/json 200
{
"ok": true,
"data": {
"id": "gid://shopify/Metaobject/456",
"state": "removed"
}
}
Example error response
application/json 404
{
"ok": false,
"error": {
"code": "ITEM_NOT_FOUND",
"message": "The requested item could not be found.",
"retryable": false
}
}
POST /toggle
ToggleRequestSchema Summary Removes if an active save exists; otherwise saves (same as save path, including DUPLICATE_SAVE if already active after a race). Body is ToggleRequestSchema (= SaveRequestSchema): context, entityType, entityGid, source.
Request body (
ToggleRequestSchema)
| Field | Type | Required | Notes |
context | string | yes | — |
entityType | EntityTypeSchema | yes | One of: product, variant, collection, metaobject, article, page, configuration. |
entityGid | string | yes | — |
source | string | yes | — |
Success data (
SaveOperationResultSchema)
| Field | Type | Required | Notes |
id | string | yes | — |
state | "active" | "removed" | yes | — |
Common error codes
-
DUPLICATE_SAVE (409) — save path if item already active -
VALIDATION_ERROR (400) -
Same upstream errors as save/remove
Plan / limits
No endpoint-specific plan tier. Monthly budgets and batch rules: see limits section.
Example request
curl
curl -sS -X POST 'https://{shop}/apps/{proxy-subpath}/toggle?shop=...&signature=...×tamp=...&logged_in_customer_id=...' \
-H 'Content-Type: application/json' \
-d '{"context":"wishlist","entityType":"product","entityGid":"gid://shopify/Product/123","source":"theme"}'
fetch
await fetch("/apps/{proxy-subpath}/toggle?shop=...&signature=...×tamp=...&logged_in_customer_id=...", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
context: "wishlist",
entityType: "product",
entityGid: "gid://shopify/Product/123",
source: "theme",
}),
});
Example response
application/json 200
{
"ok": true,
"data": {
"id": "gid://shopify/Metaobject/456",
"state": "active"
}
}
GET /list
ListQuerySchema Summary Paginated active save_items for the list context (query parameters). If no list exists yet, returns 200 with an empty items array—not an error. GET only.
Query parameters (
ListQuerySchema)
| Field | Type | Required | Notes |
context | string | yes | — |
limit | integer | no | Coerced from string; default 20, max 100. |
cursor | string | no | Opaque pagination cursor (forward). |
Success data (
ListResultSchema)
| Field | Type | Required | Notes |
items | ListItem[] | yes | ListItemSchema: id, entityType, entityGid, status; optional savedAt, snapshot (parsed JSON if valid). |
pageInfo | { hasNextPage, endCursor } | yes | endCursor: string | null. |
Common error codes
-
VALIDATION_ERROR (400) — query fails ListQuerySchema -
SHOPIFY_UPSTREAM_ERROR (502), INTERNAL_ERROR (500)
Plan / limits
No endpoint-specific plan tier. Monthly budgets and batch rules: see limits section.
Example request
curl
curl -sS 'https://{shop}/apps/{proxy-subpath}/list?shop=...&signature=...×tamp=...&logged_in_customer_id=...&context=wishlist&limit=20'
fetch
const q = new URLSearchParams({
shop: "...",
signature: "...",
timestamp: "...",
logged_in_customer_id: "...",
context: "wishlist",
limit: "20",
});
await fetch(`/apps/{proxy-subpath}/list?${q}`, { credentials: "include" });
Example response
application/json 200
{
"ok": true,
"data": {
"items": [
{
"id": "gid://shopify/Metaobject/456",
"entityType": "product",
"entityGid": "gid://shopify/Product/123",
"status": "active",
"savedAt": "2026-03-01T12:00:00.000Z"
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": null
}
}
}
POST /batch
BatchRequestSchema Summary Up to 50 operations per request (BatchRequestSchema). Each entry is BatchOperationSchema with action in: save, remove, toggle. Duplicate identity keys (same context + entityType + entityGid) in one request yield BATCH_CONFLICT (409) before any work runs. Per-operation failures are returned inside results[] with ok: false (HTTP 200 on the overall response).
Request body (
BatchRequestSchema)
| Field | Type | Required | Notes |
operations | BatchOperationSchema[] | yes | Min 1, max 50 elements. |
Each element of operations (BatchOperationSchema):
| Field | Type | Required | Notes |
action | "save" | "remove" | "toggle" | yes | — |
context | string | yes | — |
entityType | EntityTypeSchema | yes | One of: product, variant, collection, metaobject, article, page, configuration. |
entityGid | string | yes | — |
source | string | no | Use for save / toggle actions. |
Success data (
BatchResultSchema)
| Field | Type | Required | Notes |
total | integer | yes | Same as operations.length. |
succeeded | integer | yes | Count with ok: true in results. |
failed | integer | yes | Count with ok: false in results. |
results | BatchOperationResultSchema[] | yes | Discriminated on ok: success → result (SaveOperationResultSchema); failure → code, optional message, retryable. |
Common error codes
-
BATCH_CONFLICT (409) — duplicate item keys in one request -
VALIDATION_ERROR (400) — body fails BatchRequestSchema -
Per-op DUPLICATE_SAVE, ITEM_NOT_FOUND, etc. in results[] (overall HTTP 200)
Plan / limits
Free: batch not available. Starter: up to 10 operations per batch request. Pro and Scale: up to 50. When plan or budget limits apply, expect PLAN_LIMIT_REACHED (402) or related errors. The API still accepts up to 50 operations in the JSON body before plan checks.
Example request
curl
curl -sS -X POST 'https://{shop}/apps/{proxy-subpath}/batch?shop=...&signature=...×tamp=...&logged_in_customer_id=...' \
-H 'Content-Type: application/json' \
-d '{"operations":[{"action":"save","context":"wishlist","entityType":"product","entityGid":"gid://shopify/Product/1","source":"theme"},{"action":"remove","context":"wishlist","entityType":"product","entityGid":"gid://shopify/Product/2"}]}'
fetch
await fetch("/apps/{proxy-subpath}/batch?shop=...&signature=...×tamp=...&logged_in_customer_id=...", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
operations: [
{ action: "save", context: "wishlist", entityType: "product", entityGid: "gid://shopify/Product/1", source: "theme" },
{ action: "remove", context: "wishlist", entityType: "product", entityGid: "gid://shopify/Product/2" },
],
}),
});
Example response
application/json 200
{
"ok": true,
"data": {
"total": 2,
"succeeded": 1,
"failed": 1,
"results": [
{
"ok": true,
"result": {
"id": "gid://shopify/Metaobject/111",
"state": "active"
}
},
{
"ok": false,
"code": "DUPLICATE_SAVE",
"message": "The item is already saved.",
"retryable": false
}
]
}
}
Example error response
application/json 409
{
"ok": false,
"error": {
"code": "BATCH_CONFLICT",
"message": "Batch contains conflicting operations on the same item. Conflicting keys: wishlist::product::gid://shopify/Product/1",
"retryable": false
}
}
GET /is-saved
IsSavedQuerySchema Summary Returns whether an item is actively saved. No list or item yields saved: false (200). Query parameters: context, entityType, entityGid (same identity fields as remove). GET only.
Query parameters (
IsSavedQuerySchema)
| Field | Type | Required | Notes |
context | string | yes | — |
entityType | EntityTypeSchema | yes | One of: product, variant, collection, metaobject, article, page, configuration. |
entityGid | string | yes | — |
Success data (
IsSavedResultSchema)
| Field | Type | Required | Notes |
saved | boolean | yes | — |
Common error codes
-
VALIDATION_ERROR (400) — query fails IsSavedQuerySchema
Plan / limits
No endpoint-specific plan tier. Monthly budgets and batch rules: see limits section.
Example request
curl
curl -sS 'https://{shop}/apps/{proxy-subpath}/is-saved?shop=...&signature=...×tamp=...&logged_in_customer_id=...&context=wishlist&entityType=product&entityGid=gid%3A%2F%2Fshopify%2FProduct%2F123'
fetch
const params = new URLSearchParams({
shop: "...",
signature: "...",
timestamp: "...",
logged_in_customer_id: "...",
context: "wishlist",
entityType: "product",
entityGid: "gid://shopify/Product/123",
});
const res = await fetch(`/apps/{proxy-subpath}/is-saved?${params}`, {
credentials: "include",
});
const body = await res.json();
Example response
application/json 200
{
"ok": true,
"data": {
"saved": true
}
}
05 / LIMITS
Rate limits and plan-based restrictions
Each plan sets a per-shop monthly request budget, whether batch is available, and a maximum batch size (see the table below). Responses may use RATE_LIMITED (429) or PLAN_LIMIT_REACHED (402) when usage or plan caps apply. A batch request can include up to 50 operations in the JSON body; your plan may allow fewer than that.
| Plan | Monthly request budget | Batch | Max batch size (plan) |
| Free | 500 | Not enabled | — |
| Starter | 5,000 | Enabled | 10 |
| Pro | 50,000 | Enabled | 50 |
| Scale | 250,000 | Enabled | 50 |
The server accepts at most 50 operations per batch request; your plan may enforce a lower limit separately.