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 .

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 ).

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 .

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=...&timestamp=...&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=...&timestamp=...&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=...&timestamp=...&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=...&timestamp=...&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=...&timestamp=...&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=...&timestamp=...&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=...&timestamp=...&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=...&timestamp=...&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=...&timestamp=...&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=...&timestamp=...&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.