{"app":{"handle":"matchly","name":"Matchly"},"generatedAt":"2026-11-02T12:07:43Z","version":"1.0.0","baseUrl":"https:\/\/api.sappo.xco.agency","specUrl":"https:\/\/api.sappo.xco.agency\/matchly\/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 Matchly API lets you read your own store's Matchly 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\/matchly\/v1\/{resource}`\n- **Authentication** \u2014 `Authorization: Bearer mak_\u2026`\n- **Machine-readable reference** \u2014 [OpenAPI 3.0 document](https:\/\/api.sappo.xco.agency\/matchly\/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\/matchly\/v1\/quizzes\" \\\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\/matchly\/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\/matchly\/v1\/quizzes?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\/matchly\/docs` | This page |\n| `GET https:\/\/api.sappo.xco.agency\/matchly\/docs.json` | OpenAPI 3.0 document |\n| `GET https:\/\/api.sappo.xco.agency\/matchly\/docs\/content.json` | This documentation as JSON |\n| `GET https:\/\/api.sappo.xco.agency\/matchly\/docs\/mcp` | The MCP guide |\n| `GET https:\/\/api.sappo.xco.agency\/matchly\/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\/matchly\/v1\/quizzes?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\/matchly\/v1\/quizzes?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\/matchly\/v1\/quizzes\" \\\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\/matchly\/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\nMatchly exposes the same data as this API over MCP at `https:\/\/api.sappo.xco.agency\/matchly\/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\/matchly\/docs\/mcp), with a\nmachine-readable copy at\n[`https:\/\/api.sappo.xco.agency\/matchly\/docs\/mcp.json`](https:\/\/api.sappo.xco.agency\/matchly\/docs\/mcp.json)."}],"resources":[{"name":"quizzes","title":"Quizzes","description":"Quizzes for the authenticated shop. Editor drafts and theme JSON are omitted.","endpoints":[{"method":"GET","path":"\/matchly\/v1\/quizzes","summary":"List quizzes","description":"Quizzes for the authenticated shop. Editor drafts and theme JSON are omitted.","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`."}],"exampleRequest":"curl -X GET \"https:\/\/api.sappo.xco.agency\/matchly\/v1\/quizzes?page=1&page_size=25\" -H \"Authorization: Bearer mak_your_token_here\" -H \"Accept: application\/json\"","exampleResponse":{"data":[],"meta":{"page":1,"pageSize":25,"total":0}}}]},{"name":"responses","title":"Responses","description":"Quiz responses for quizzes owned by the authenticated shop.","endpoints":[{"method":"GET","path":"\/matchly\/v1\/responses","summary":"List quiz responses","description":"Quiz responses for quizzes owned by the authenticated shop.","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`."}],"exampleRequest":"curl -X GET \"https:\/\/api.sappo.xco.agency\/matchly\/v1\/responses?page=1&page_size=25\" -H \"Authorization: Bearer mak_your_token_here\" -H \"Accept: application\/json\"","exampleResponse":{"data":[],"meta":{"page":1,"pageSize":25,"total":0}}}]},{"name":"products","title":"Products","description":"Products recommended by this shop's quiz responses, with recommendation and conversion counts.","endpoints":[{"method":"GET","path":"\/matchly\/v1\/products","summary":"List recommended products","description":"Products recommended by this shop's quiz responses, with recommendation and conversion counts.","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`."}],"exampleRequest":"curl -X GET \"https:\/\/api.sappo.xco.agency\/matchly\/v1\/products?page=1&page_size=25\" -H \"Authorization: Bearer mak_your_token_here\" -H \"Accept: application\/json\"","exampleResponse":{"data":[],"meta":{"page":1,"pageSize":25,"total":0}}}]}],"mcp":{"available":true,"endpoint":"https:\/\/api.sappo.xco.agency\/matchly\/mcp","instructions":"Quizzes, responses, and recommended products for the authenticated shop.\n\nThe store is identified automatically from your API token.\n\nAvailable tools:\n- `list_quizzes`: List quizzes\n- `get_quiz`: Get one quiz by id, including questions and answer labels\n- `get_quiz_stats`: Count quizzes and responses for the authenticated shop, optionally for one quiz\n- `list_responses`: List quiz responses\n- `get_response`: Get one quiz response by id\n- `list_products`: List products recommended by this shop's quizzes\n\nEvery `list_` tool takes `page` and `page_size` (25 by default, 100 at most) and\nreturns the page plus `meta.total`. Every `get_` tool takes the record `id`.\nEverything is read-only: changes are made in the app, never through this server.","clientConfig":{"claudeDesktop":{"mcpServers":{"sappo-matchly":{"type":"http","url":"https:\/\/api.sappo.xco.agency\/matchly\/mcp","headers":{"Authorization":"Bearer mak_your_token_here"}}}},"cli":"claude mcp add --transport http sappo-matchly https:\/\/api.sappo.xco.agency\/matchly\/mcp --header \"Authorization: Bearer mak_your_token_here\""},"tools":[{"name":"list_quizzes","title":"List Quizzes Tool","description":"List quizzes for the authenticated shop.","inputSchema":{"properties":{"page":{"description":"Page number, starting at 1","type":"integer"},"page_size":{"description":"Page size, maximum 100","type":"integer"}},"type":"object"},"examplePrompt":"Show me the quizzes."},{"name":"get_quiz","title":"Get Quiz Tool","description":"Get one quiz by id, including questions and answer labels.","inputSchema":{"properties":{"id":{"description":"Record id","type":"string"}},"type":"object","required":["id"]},"examplePrompt":"Give me the details of a quiz."},{"name":"get_quiz_stats","title":"Get Quiz Stats Tool","description":"Count quizzes and responses for the authenticated shop, optionally for one quiz.","inputSchema":{"properties":{"quiz_id":{"description":"Limit the counts to one quiz id","type":"string"}},"type":"object"},"examplePrompt":"Give me the details of the quiz stats."},{"name":"list_responses","title":"List Responses Tool","description":"List quiz responses for the authenticated shop.","inputSchema":{"properties":{"page":{"description":"Page number, starting at 1","type":"integer"},"page_size":{"description":"Page size, maximum 100","type":"integer"}},"type":"object"},"examplePrompt":"Show me the responses."},{"name":"get_response","title":"Get Response Tool","description":"Get one quiz response by id.","inputSchema":{"properties":{"id":{"description":"Record id","type":"string"}},"type":"object","required":["id"]},"examplePrompt":"Give me the details of a response."},{"name":"list_products","title":"List Products Tool","description":"List products recommended by this shop's quizzes.","inputSchema":{"properties":{"page":{"description":"Page number, starting at 1","type":"integer"},"page_size":{"description":"Page size, maximum 100","type":"integer"}},"type":"object"},"examplePrompt":"Show me the products."}]},"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 'matchly'.","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\/matchly\/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."}]}