{"app":{"handle":"product-maestro","name":"Product Maestro"},"generatedAt":"2026-11-02T12:21:58Z","version":"1.0.0","baseUrl":"https:\/\/api.sappo.xco.agency","specUrl":"https:\/\/api.sappo.xco.agency\/product-maestro\/docs.json","auth":{"tokenPrefix":"mak_","header":"Authorization","scheme":"Bearer","whereToGet":"Open the app in your Shopify admin, go to Settings \u2192 API Access, and create a token. The full token is shown once, at creation time; afterwards only its first 12 characters are stored in readable form. If that card is locked, API access is not included in your current plan and the app will not create a token until the plan changes. At most three tokens can be active at once, so revoke one you no longer use before creating another.","plan":{"required":false,"plans":[]}},"rateLimit":{"requests":60,"windowSeconds":60,"headers":["X-RateLimit-Limit","X-RateLimit-Remaining","X-RateLimit-Reset","Retry-After"]},"sections":[{"id":"overview","title":"Overview","markdown":"The Product Maestro API lets you read your own store's Product Maestro data from your\nown code: a spreadsheet sync, an internal dashboard, a warehouse system, or\nan AI assistant.\n\nIt is a JSON API over HTTPS. Every request is authenticated with a token\nyou create yourself inside the app, and every request only ever sees the\nstore that token belongs to \u2014 there is no way to ask for another store's\ndata, and no store id to pass.\n\n- **Base URL** \u2014 `https:\/\/api.sappo.xco.agency`\n- **Shape** \u2014 `GET https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/{resource}`\n- **Authentication** \u2014 `Authorization: Bearer mak_\u2026`\n- **Machine-readable reference** \u2014 [OpenAPI 3.0 document](https:\/\/api.sappo.xco.agency\/product-maestro\/docs.json)\n\nResponses are always JSON. A successful resource response carries a `data`\narray and a `meta` object; a failed one carries a single `error` object and\nnothing else.\n\nNo endpoint changes your data: everything here is a read."},{"id":"authentication","title":"Authentication","markdown":"### Get a token\n\nOpen the app in your Shopify admin, go to Settings \u2192 API Access, and create a token. The full token is shown once, at creation time; afterwards only its first 12 characters are stored in readable form. If that card is locked, API access is not included in your current plan and the app will not create a token until the plan changes. At most three tokens can be active at once, so revoke one you no longer use before creating another.\n\nA token can be given an optional expiry date, and can be revoked at any time\nfrom the same screen. Revoking takes effect on the next request.\n\n### Send the token\n\nPut it in the `Authorization` header of every request, after the word\n`Bearer`:\n\n```bash\ncurl \"https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/schedules\" \\\n  -H \"Authorization: Bearer mak_your_token_here\"\n```\n\nThere is no other accepted form: a query-string token, a cookie, or a\ndifferent header name is rejected with `MISSING_TOKEN`.\n\n### Keep it secret\n\nThe token carries everything this API can do with your whole store's data in\nthis app, so treat it like a password:\n\n- Store it in a server-side secret, not in a browser, a mobile app, or a\n  public repository.\n- Never call this API directly from a storefront page \u2014 the token would be\n  visible to every visitor.\n- Create one token per integration, so you can revoke a single one without\n  breaking the others.\n\nA token is stored only as a SHA-256 hash, so nobody \u2014 including us \u2014 can\nread it back to you. If you lose it, create a new one and revoke the old.\n\n"},{"id":"requests","title":"Making a request","markdown":"Every resource endpoint is a `GET` on a resource list:\n\n```\nGET https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/{resource}\n```\n\n`{resource}` is one of the names listed under **Resources** below. The `v1`\nsegment is the API version (see **Versioning**).\n\nAsk for JSON and read JSON back:\n\n```bash\ncurl \"https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/schedules?page=1&page_size=50\" \\\n  -H \"Authorization: Bearer mak_your_token_here\" \\\n  -H \"Accept: application\/json\"\n```\n\nA successful response is always this shape:\n\n```json\n{\n  \"data\": [ { \"id\": 1 } ],\n  \"meta\": { \"page\": 1, \"pageSize\": 50, \"total\": 137 }\n}\n```\n\n`data` holds the records for the page you asked for, in the resource's own\nfield names. `meta` describes the page.\n\nEach resource's example response below is **abridged**: a record carries\nevery column stored for it, so expect fields the example does not show.\nSome resources return customer details, email addresses or codes among\nthem \u2014 read the resource's own description before you log, cache or forward\na response.\n\nThese documentation endpoints need no token at all, so you can explore\nbefore you create one:\n\n| Endpoint | What it returns |\n| --- | --- |\n| `GET https:\/\/api.sappo.xco.agency\/product-maestro\/docs` | This page |\n| `GET https:\/\/api.sappo.xco.agency\/product-maestro\/docs.json` | OpenAPI 3.0 document |\n| `GET https:\/\/api.sappo.xco.agency\/product-maestro\/docs\/content.json` | This documentation as JSON |\n| `GET https:\/\/api.sappo.xco.agency\/product-maestro\/docs\/mcp` | The MCP guide |\n| `GET https:\/\/api.sappo.xco.agency\/product-maestro\/docs\/mcp.json` | The MCP guide as JSON |"},{"id":"pagination","title":"Pagination","markdown":"Lists are paged with two query parameters:\n\n| Parameter | Type | Default | Notes |\n| --- | --- | --- | --- |\n| `page` | integer | `1` | 1-based. Anything below 1 is treated as 1. |\n| `page_size` | integer | `25` | Capped at `100`. A larger value is silently reduced to `100`. |\n\nNote the asymmetry: the request parameter is `page_size` (snake case), while\nthe field echoed back in `meta` is `pageSize` (camel case).\n\n```json\n\"meta\": { \"page\": 2, \"pageSize\": 25, \"total\": 137 }\n```\n\n`total` is the number of records matching your filters, not the number on\nthis page. Walk the list until `page * pageSize >= total`:\n\n```bash\n# 137 records at 25 per page = 6 pages\nfor page in 1 2 3 4 5 6; do\n  curl -s \"https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/schedules?page=$page&page_size=25\" \\\n    -H \"Authorization: Bearer mak_your_token_here\"\ndone\n```\n\nRecords are returned newest first. Which timestamp \"newest\" means depends on\nthe resource \u2014 creation time for most, last-change or scheduled time for a\nfew \u2014 and each resource's description below says which. Because new records\ncan arrive while you page, a long walk can show a record twice or skip one;\nif that matters, page from the last page backwards, or filter to a window\nthat has stopped changing."},{"id":"filtering","title":"Filtering","markdown":"Any query parameter other than `page` and `page_size` is passed to the\nresource as a filter. Each resource accepts its own set \u2014 the supported\nnames, types and allowed values are listed per resource under **Resources**\nbelow, and in the OpenAPI document.\n\n```bash\n# Only the records a resource's own `status` filter matches\ncurl \"https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/schedules?status=PENDING\" \\\n  -H \"Authorization: Bearer mak_your_token_here\"\n```\n\nThree things to know:\n\n- **An unknown filter is ignored, not rejected.** A misspelled parameter\n  returns the unfiltered list with `200`, so check your spelling against the\n  resource's parameter list if a result looks too large.\n- **Filters combine with AND.** Passing two filters narrows the list to\n  records matching both.\n- **Booleans accept the usual spellings** \u2014 `true`, `1`, `yes`, `on` are\n  true; anything else is false.\n\nFilters apply before paging, so `meta.total` reflects the filtered count."},{"id":"errors","title":"Errors","markdown":"Every failure of a resource endpoint returns the same envelope, with the\nHTTP status repeated inside the body so it survives logging:\n\n```json\n{\n  \"error\": {\n    \"code\": \"INVALID_TOKEN\",\n    \"message\": \"Token is invalid, expired, or revoked.\",\n    \"status\": 401\n  }\n}\n```\n\nBranch on `error.code`, never on `error.message` \u2014 the wording may be\nimproved, and we aim to keep the codes stable within a version. The table\nunder this section lists every code a resource endpoint can return, and what\nto do about each.\n\n\n\nTwo cases are worth calling out because they look like one another:\n\n- `MISSING_TOKEN` means the `Authorization` header was absent or did not\n  begin with `Bearer `. Your request never reached a token lookup.\n- `INVALID_TOKEN` means a token was sent but did not pass: it does not\n  exist, it was revoked, it expired, or the store is suspended. The API\n  deliberately does not tell you which, so a token cannot be probed."},{"id":"rate-limits","title":"Rate limits","markdown":"Each token may make **60 requests per 60\nseconds**. The budget is per token, not per IP address, so splitting work\nacross machines does not raise it \u2014 creating a second token for a second\nintegration does.\n\nEvery successful response carries your current standing:\n\n| Header | Meaning |\n| --- | --- |\n| `X-RateLimit-Limit` | Requests allowed per window (`60`). |\n| `X-RateLimit-Remaining` | Requests left in the current window. |\n| `X-RateLimit-Reset` | Unix timestamp one full window after this request. On a `429` it is when you may retry. |\n\nOver the limit you get `429` with `error.code` of `RATE_LIMITED`, plus a\n`Retry-After` header holding the number of seconds to wait. The `429`\nresponse repeats `X-RateLimit-Limit`, `X-RateLimit-Remaining` (always `0`)\nand `X-RateLimit-Reset`.\n\nHonour `Retry-After` rather than retrying immediately:\n\n```bash\nresponse=$(curl -s -D headers.txt \"https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/schedules\" \\\n  -H \"Authorization: Bearer mak_your_token_here\")\n# on 429: sleep \"$(grep -i '^retry-after:' headers.txt | tr -dc '0-9')\"\n```\n\nFor a large export, prefer a bigger `page_size` over more requests: one\nrequest for `100` records costs the same budget as one request\nfor a single record.\n\nThe documentation endpoints are not metered by this budget."},{"id":"versioning","title":"Versioning","markdown":"The version is the `v1` segment in the path, and this documentation\ndescribes release **1.0.0**.\n\nInside `v1` we aim to make only additive changes: a new resource, a new\nfilter, or a new field on a record. Treat unknown fields as something to\nignore rather than something to fail on, and your integration will keep\nworking.\n\nA change that could break a reader \u2014 a field removed or renamed, a filter's\nmeaning changed, a status value dropped \u2014 belongs on a new path segment\n(`v2`), with `v1` still answering. Bug fixes are the exception we do make\ninside `v1`: where a list was ordered by the wrong column, correcting it\nchanges what page one returns.\n\n`info.version` in the [OpenAPI document](https:\/\/api.sappo.xco.agency\/product-maestro\/docs.json) is the documentation\nrelease; the path segment is the API contract. Pin the path segment in your\ncode, and re-read the document when `info.version` changes."},{"id":"mcp","title":"MCP for AI assistants","markdown":"The **Model Context Protocol** (MCP) is an open standard that lets an AI\nassistant call tools on a server instead of guessing from text: you connect\nthe assistant once, it discovers which tools exist, and then it can answer\n\"how many claims are still pending?\" by actually querying your store rather\nthan inventing a number. Claude Desktop, Claude Code and a growing list of\nother clients speak it.\n\nProduct Maestro exposes the same data as this API over MCP at `https:\/\/api.sappo.xco.agency\/product-maestro\/mcp`,\nauthenticated with the same `mak_` token in the same\n`Authorization` header. No separate signup, no second credential.\n\nThe tool list, the exact client configuration to paste, and the errors to\nexpect are on the [MCP guide](https:\/\/api.sappo.xco.agency\/product-maestro\/docs\/mcp), with a\nmachine-readable copy at\n[`https:\/\/api.sappo.xco.agency\/product-maestro\/docs\/mcp.json`](https:\/\/api.sappo.xco.agency\/product-maestro\/docs\/mcp.json)."}],"resources":[{"name":"schedules","title":"Schedules","description":"Your product schedules, newest first by `createdAt`. Each record\ncarries its `actions` relation \u2014 the individual operations the\nschedule performs.\n\n`selectionType` says what the schedule targets (products, a\ncollection, or an advanced filter) and `selectionIds` holds the\nchosen ids. `conditions` is the JSON rule set for an advanced\nselection.","endpoints":[{"method":"GET","path":"\/product-maestro\/v1\/schedules","summary":"List product schedules","description":"Your product schedules, newest first by `createdAt`. Each record\ncarries its `actions` relation \u2014 the individual operations the\nschedule performs.\n\n`selectionType` says what the schedule targets (products, a\ncollection, or an advanced filter) and `selectionIds` holds the\nchosen ids. `conditions` is the JSON rule set for an advanced\nselection.","parameters":[{"name":"page","in":"query","type":"integer","required":false,"description":"1-based page number. Values below 1 are treated as 1."},{"name":"page_size","in":"query","type":"integer","required":false,"description":"Records per page. Values above 100 are reduced to 100. Echoed back as `meta.pageSize`."},{"name":"status","in":"query","type":"string","required":false,"description":"Only schedules in this status. Matched exactly, so it must be upper-case \u2014 unlike the actions resource, this filter does not upper-case the value for you. Common values are `SCHEDULED`, `PENDING`, `PROCESSING`, `COMPLETED`, `FAILED`, `CANCELED` and `RECURRING`; the app owns this column and can store a value this list does not name, so do not reject an unknown one."},{"name":"archived","in":"query","type":"boolean","required":false,"description":"`true` for archived schedules only, `false` for schedules that are not archived. Omit the filter to get both."}],"exampleRequest":"curl -X GET \"https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/schedules?page=1&page_size=25&status={value}\" -H \"Authorization: Bearer mak_your_token_here\" -H \"Accept: application\/json\"","exampleResponse":{"data":[{"id":204,"title":"Autumn markdown","description":null,"status":"COMPLETED","selectionType":"PRODUCTS","selectionIds":["gid:\/\/shopify\/Product\/7421004382209"],"conditions":null,"isBulk":false,"archived":false,"archivedAt":null,"createdAt":"2026-09-25T10:11:00.000000Z","actions":[{"id":8841,"type":"SET_PRICE","status":"COMPLETED"}]}],"meta":{"page":1,"pageSize":25,"total":58}}}]},{"name":"actions","title":"Actions","description":"The individual actions that make up your schedules \u2014 one row per\nthing Product Maestro will do, or has done \u2014 newest first by\n`createdAt`. Each record carries its parent schedule as\n`product_schedule`.\n\n`scheduledAt` is when the action runs, `startedAt` and `completedAt`\nare filled in as it goes, and `parameters` is a JSON object whose\nkeys depend on `type` (a price action carries an amount, a tag action\ncarries tags). A recurring action keeps `repeat`, `publishedCount`\nand `nextPublishDate`.","endpoints":[{"method":"GET","path":"\/product-maestro\/v1\/actions","summary":"List schedule actions","description":"The individual actions that make up your schedules \u2014 one row per\nthing Product Maestro will do, or has done \u2014 newest first by\n`createdAt`. Each record carries its parent schedule as\n`product_schedule`.\n\n`scheduledAt` is when the action runs, `startedAt` and `completedAt`\nare filled in as it goes, and `parameters` is a JSON object whose\nkeys depend on `type` (a price action carries an amount, a tag action\ncarries tags). A recurring action keeps `repeat`, `publishedCount`\nand `nextPublishDate`.","parameters":[{"name":"page","in":"query","type":"integer","required":false,"description":"1-based page number. Values below 1 are treated as 1."},{"name":"page_size","in":"query","type":"integer","required":false,"description":"Records per page. Values above 100 are reduced to 100. Echoed back as `meta.pageSize`."},{"name":"status","in":"query","type":"string","required":false,"description":"Only actions in this status. Case-insensitive \u2014 the value is upper-cased before it is matched. Common values are `SCHEDULED`, `PENDING`, `PROCESSING`, `COMPLETED`, `FAILED`, `CANCELED`, `SKIPPED`, `STOPPED` and `RECURRING`; the app owns this column and can store a value this list does not name, so do not reject an unknown one."},{"name":"type","in":"query","type":"string","required":false,"description":"Only actions of this type. Matched exactly, so it must be upper-case. Common values are `SET_PRICE`, `INCREASE_PRICE`, `DECREASE_PRICE`, `ADD_TAG`, `REMOVE_TAG`, `PUBLISH`, `DRAFT`, `ARCHIVE`, `SET_INVENTORY` and `PRODUCT_SET`; the list is not exhaustive \u2014 the app adds types as it gains features, and `GET \/product-maestro\/v1\/actions` shows which ones your own schedules use."}],"exampleRequest":"curl -X GET \"https:\/\/api.sappo.xco.agency\/product-maestro\/v1\/actions?page=1&page_size=25&status={value}\" -H \"Authorization: Bearer mak_your_token_here\" -H \"Accept: application\/json\"","exampleResponse":{"data":[{"id":8841,"scheduleId":204,"status":"COMPLETED","type":"SET_PRICE","scheduledAt":"2026-09-30T06:00:00.000000Z","startedAt":"2026-09-30T06:00:04.000000Z","completedAt":"2026-09-30T06:00:31.000000Z","parameters":{"price":"19.99"},"repeat":"NONE","publishedCount":1,"nextPublishDate":null,"createdAt":"2026-09-25T10:11:00.000000Z","product_schedule":{"id":204,"title":"Autumn markdown","status":"COMPLETED"}}],"meta":{"page":1,"pageSize":25,"total":913}}}]}],"mcp":{"available":true,"endpoint":"https:\/\/api.sappo.xco.agency\/product-maestro\/mcp","instructions":"Product Maestro MCP server for accessing product schedule data from Shopify stores.\n\nThe store is identified automatically from your API token.\n\nAvailable tools:\n- `list_schedules`: List product schedules with optional status\/archived filters and pagination\n- `get_schedule`: Get a specific schedule by ID with its actions\n- `list_actions`: List schedule actions with optional status\/type filters and pagination\n- `create_draft_schedule`: Create a draft schedule from structured actions (needs the schedules:write scope). Drafts never run on their own\n- `execute_schedule`: Always refuses. Schedules are never run over the API\n\nDrafting and running are deliberately separate. A draft runs only after the merchant reviews it in Product Maestro and clicks Save, so tell the merchant to do that rather than looking for a way to run it.","clientConfig":{"claudeDesktop":{"mcpServers":{"sappo-product-maestro":{"type":"http","url":"https:\/\/api.sappo.xco.agency\/product-maestro\/mcp","headers":{"Authorization":"Bearer mak_your_token_here"}}}},"cli":"claude mcp add --transport http sappo-product-maestro https:\/\/api.sappo.xco.agency\/product-maestro\/mcp --header \"Authorization: Bearer mak_your_token_here\""},"tools":[{"name":"list_schedules","title":"List Schedules Tool","description":"List product schedules for a Shopify store. Returns paginated schedules with their associated actions.","inputSchema":{"properties":{"page":{"description":"Page number (default: 1)","type":"integer"},"page_size":{"description":"Items per page, max 100 (default: 25)","type":"integer"},"status":{"description":"Filter by status","type":"string"},"archived":{"description":"Filter by archived state","type":"boolean"}},"type":"object"},"examplePrompt":"Show me the schedules."},{"name":"get_schedule","title":"Get Schedule Tool","description":"Get a single product schedule by ID for a Shopify store, including its actions.","inputSchema":{"properties":{"id":{"description":"Schedule ID","type":"integer"}},"type":"object","required":["id"]},"examplePrompt":"Give me the details of a schedule."},{"name":"list_actions","title":"List Actions Tool","description":"List product schedule actions for a Shopify store using Product Maestro.","inputSchema":{"properties":{"page":{"description":"Page number (default: 1)","type":"integer"},"page_size":{"description":"Items per page, max 100 (default: 25)","type":"integer"},"status":{"description":"Filter by action status","enum":["SCHEDULED","PENDING","IN_PROGRESS","COMPLETED","FAILED","RECURRING"],"type":"string"},"type":{"description":"Filter by action type","type":"string"}},"type":"object"},"examplePrompt":"Show me the actions."},{"name":"create_draft_schedule","title":"Create Draft Schedule Tool","description":"Create a draft product schedule for the store. Drafts never run on their own: the merchant must open the draft in Product Maestro, review it, and click Save. Actions that share a selection land on one draft; a different selection gets its own draft.\n\nEach action needs actionType + targetType from the list below, a selectionType valid for that target (PRODUCT: PRODUCTS, COLLECTION or ADVANCED; COLLECTION: COLLECTION; VARIANTS: VARIANTS; PRICE_LIST: PRICE_LIST), parameters matching the shape shown, and scheduledAtLocal as the store's local wall-clock time (\"2026-10-03T09:00\", no offset). filterConditions are only allowed with selectionType ADVANCED.\n\nSupported actions (actionType (targets) \u2014 parameters):\n- ADD_TAG (PRODUCT) \u2014 parameters: { tags: string[] }\n- REMOVE_TAG (PRODUCT) \u2014 parameters: { tags: string[] }\n- PUBLISH (PRODUCT) \u2014 parameters: {} \u2014 no fields\n- DRAFT (PRODUCT) \u2014 parameters: {} \u2014 no fields\n- ARCHIVE (PRODUCT) \u2014 parameters: {} \u2014 no fields\n- UPDATE_TEMPLATE_SUFFIX (PRODUCT\/COLLECTION) \u2014 parameters: { templateSuffix: string }\n- JOIN_COLLECTION (PRODUCT) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- LEAVE_COLLECTION (PRODUCT) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- UPDATE_PRODUCT_METAFIELDS (PRODUCT\/COLLECTION) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- UNPUBLISH_FROM_CHANNEL (PRODUCT\/COLLECTION) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- PUBLISH_INTO_CHANNEL (PRODUCT\/COLLECTION) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- INCREASE_PRICE (PRODUCT) \u2014 parameters: { amount: number, amount_type: \"fixed\"|\"percentage\", round?: \"up\"|\"down\"|\"closest\" } \u2014 round only applies when amount_type is \"percentage\"\n- DECREASE_PRICE (PRODUCT) \u2014 parameters: { amount: number, amount_type: \"fixed\"|\"percentage\", round?: \"up\"|\"down\"|\"closest\" } \u2014 round only applies when amount_type is \"percentage\"; the reverse of this action is INCREASE_PRICE with the same amount\/amount_type\n- INCREASE_INVENTORY (PRODUCT) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- DECREASE_INVENTORY (PRODUCT) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- SET_INVENTORY (PRODUCT) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- SET_PRICE (PRODUCT) \u2014 parameters: { amount: number }\n- SET_PRICE_DECIMAL (PRODUCT) \u2014 parameters: { amount: number }\n- UPDATE_TITLE (PRODUCT\/COLLECTION) \u2014 parameters: { title: string }\n- UPDATE_COLLECTION_SORT (COLLECTION) \u2014 parameters: { collectionSort: string }\n- ROUND_PRICE (PRODUCT) \u2014 parameters: { round: \"up\"|\"down\"|\"closest\" }\n- UPDATE_BODY_HTML (PRODUCT\/COLLECTION) \u2014 parameters: { bodyHtml: string } \u2014 an empty string is a valid instruction: it clears the description\n- DELETE_PRODUCT (PRODUCT) \u2014 parameters: {} \u2014 no fields; only propose this if the merchant explicitly asked to delete, never as a guess\n- DELETE_COLLECTION (COLLECTION) \u2014 parameters: {} \u2014 no fields; only propose this if the merchant explicitly asked to delete, never as a guess\n- PRODUCT_SET (PRODUCT) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- REORDER_COLLECTION_PRODUCTS (COLLECTION) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- PRICE_LIST_UPDATE (PRICE_LIST) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- PRICE_LIST_FIXED_PRICES_ADD (PRICE_LIST) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- PRICE_LIST_FIXED_PRICES_BY_PRODUCT_UPDATE (PRICE_LIST) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- PRICE_LIST_FIXED_PRICES_DELETE (PRICE_LIST) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- PRICE_LIST_FIXED_PRICES_UPDATE (PRICE_LIST) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- UPDATE_FEATURED_IMAGE (PRODUCT\/COLLECTION) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- UPDATE_VARIANT (VARIANTS) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- UNLIST (PRODUCT) \u2014 parameters: {} \u2014 no fields\n- UPDATE_COLLECTION_SEO (COLLECTION) \u2014 parameters: { title?: string, description?: string } \u2014 at least one is required\n- UPDATE_PRODUCT_SEO (PRODUCT) \u2014 parameters: { title?: string, description?: string } \u2014 at least one is required\n- PRODUCT_MEDIA_UPDATE (PRODUCT) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- ADD_VARIANT (VARIANTS) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- REMOVE_VARIANT (VARIANTS) \u2014 parameters: {} \u2014 shape not documented yet, ask for nothing beyond selection\/date\n- PUBLISH_INTO_MARKET (PRODUCT) \u2014 parameters: { markets: array } \u2014 one or more market identifiers named by the merchant\n- UNPUBLISH_FROM_MARKET (PRODUCT) \u2014 parameters: { markets: array } \u2014 one or more market identifiers named by the merchant","inputSchema":{"properties":{"summary":{"description":"One line describing the intended change. The merchant sees it on the draft when reviewing.","type":"string"},"actions":{"minItems":1,"maxItems":10,"items":{"properties":{"actionType":{"type":"string"},"targetType":{"enum":["PRODUCT","COLLECTION","PRICE_LIST","VARIANTS"],"type":"string"},"selectionType":{"enum":["PRODUCTS","COLLECTION","VARIANTS","PRICE_LIST","ADVANCED"],"type":"string"},"selectionQuery":{"description":"What the action applies to, in words, e.g. \"Winter Coats collection\". The merchant picks the exact items when reviewing.","type":"string"},"filterConditions":{"items":{"properties":{"type":{"enum":["not_sold_days","total_orders"],"type":"string"},"operator":{"enum":["gt","gte","lt","lte","eq"],"type":"string"},"value":{"type":"number"}},"type":"object","required":["type","operator","value"]},"type":"array"},"parameters":{"description":"The parameters shape for this actionType, from the tool description.","type":"object"},"scheduledAtLocal":{"description":"Store-local wall-clock time, no offset, e.g. \"2026-10-03T09:00\".","type":"string"},"repeat":{"enum":["NONE","HOURLY","DAILY","WEEKLY","BI_WEEKLY","MONTHLY"],"type":"string"}},"type":"object","required":["actionType","targetType","selectionType","scheduledAtLocal"]},"type":"array"}},"type":"object","required":["summary","actions"]},"examplePrompt":"Use create_draft_schedule to tell me about the draft schedule."},{"name":"execute_schedule","title":"Execute Schedule Tool","description":"Product Maestro does not run schedules over the API. This always refuses, and explains that the merchant must review the draft in the app and click Save; it then runs at its scheduled time.","inputSchema":{"properties":{"id":{"description":"Schedule ID","type":"integer"}},"type":"object"},"examplePrompt":"Use execute_schedule to tell me about the schedule."}]},"errors":[{"status":400,"code":"INVALID_PAGE","message":"Query parameter page must be a positive integer.","whatToDo":"Send `page` as a whole number of 1 or more, or leave it out for the first page. A page past the end is not an error: it answers 200 with an empty `data` array."},{"status":400,"code":"INVALID_PAGE_SIZE","message":"Query parameter page_size must be an integer between 1 and 100.","whatToDo":"Send `page_size` as a whole number between 1 and 100. Leave it out for the default of 25. Asking for more than the maximum is refused rather than quietly reduced, so a client cannot believe it received a bigger page than it did."},{"status":401,"code":"MISSING_TOKEN","message":"Authorization header with Bearer token is required.","whatToDo":"Send an `Authorization: Bearer mak_\u2026` header. This code also appears when the header is present but does not start with `Bearer ` \u2014 a bare token or a `Token \u2026` prefix is not accepted."},{"status":401,"code":"INVALID_TOKEN","message":"Token is invalid, expired, or revoked.","whatToDo":"Check the token was copied in full, is still listed as active in Settings \u2192 API Access, and has not passed its expiry date. Create a new token if in doubt. The same code is returned when the store is suspended."},{"status":403,"code":"PLAN_REQUIRED","message":"API access is not included in the current plan.","whatToDo":"The token is valid, but this app sells API access on named plans only and this store is not on one of them. The plans that carry it are listed under **Authentication** above; change the plan in the app and the same token starts working \u2014 there is no need to create a new one."},{"status":403,"code":"INSUFFICIENT_SCOPE","message":"This token does not include the {resource}:read scope.","whatToDo":"The token is valid and the plan allows the API, but this token was created without the scope for that resource. Scopes are fixed when a token is created, so create a new one that includes it \u2014 a token minted before scopes existed carries the wildcard and is unaffected."},{"status":404,"code":"APP_NOT_FOUND","message":"App not found.","whatToDo":"The app handle in the path is not one Sappo serves. Use the handle this documentation was generated for, and check for a typo or a stray path segment."},{"status":404,"code":"RESOURCE_NOT_FOUND","message":"Resource '{resource}' is not available for 'product-maestro'.","whatToDo":"The app is valid but it does not expose that resource. Use one of the resource names listed in this documentation, or read them from `https:\/\/api.sappo.xco.agency\/product-maestro\/docs.json`."},{"status":404,"code":"NOT_FOUND","message":"No record found for id {id}.","whatToDo":"The resource exists but holds no record with that id for your store. Check the id came from this app and this store \u2014 ids are not shared between apps. This is the single-record form; a list request with no matches answers 200 with an empty `data` array instead."},{"status":429,"code":"RATE_LIMITED","message":"Too many requests. Please slow down.","whatToDo":"Wait the number of seconds in the `Retry-After` header before retrying, and keep each client under 60 requests per 60 seconds. Fetch more records per request instead of making more requests."},{"status":500,"code":"INTERNAL_ERROR","message":"An internal error occurred.","whatToDo":"The request was accepted but failed on our side; it has been reported to us automatically. Retry once after a short pause, and if it persists contact support with the time of the request and the path you called."},{"status":503,"code":"API_NOT_AVAILABLE","message":"The API is not available for this app yet.","whatToDo":"The app is served but its store of API tokens has not been created yet, so no token can be checked. Nothing is wrong with your request: retry later, and tell support if it lasts."}]}