{"openapi":"3.1.0","info":{"title":"Scanlo API","version":"1.0.0","description":"Tracked QR codes and hubs for small service businesses. Every endpoint is also a tool on the Scanlo MCP server (x-mcp-tool), with the same arguments and the same plan limits. Nothing can be deleted through the API.","contact":{"name":"Scanlo","url":"https://scanlo.io/contact"}},"externalDocs":{"description":"Developer docs","url":"https://scanlo.io/developers"},"servers":[{"url":"https://api.scanlo.io"}],"security":[{"oauth2":[]}],"paths":{"/v1/account":{"get":{"operationId":"get_account","summary":"Get account","description":"The signed-in Scanlo account: plan, how many QR codes and hubs it uses and may have, and how far back its scan stats go. Call this first, and to explain why a create was refused.","x-mcp-tool":"get_account","responses":{"200":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/codes":{"get":{"operationId":"list_codes","summary":"List QR codes","description":"Every QR code and hub on the account, with its short URL, destination, status and a link to its QR image (SVG).","x-mcp-tool":"list_codes","responses":{"200":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"create_code","summary":"Create QR code","description":"Create a tracked QR code that sends every scan to one link. Types: review (a Google or other review page), booking (a booking page), payment (one payment link). For several payment or review options behind one code, use create_hub. The destination is checked for safety; plan limits apply.","x-mcp-tool":"create_code","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["review","booking","payment"],"description":"What the code is for."},"name":{"type":"string","minLength":1,"maxLength":80,"description":"Internal name, shown only in the dashboard."},"destination_url":{"type":"string","minLength":1,"maxLength":2048,"description":"Where a scan goes. https:// is added if missing."},"placement":{"type":"string","maxLength":80,"description":"Where the code is placed, e.g. \"front desk\" or \"receipt\". Placement reports group by it."}},"required":["type","name","destination_url"],"additionalProperties":false}}}},"responses":{"201":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/codes/{id}":{"get":{"operationId":"get_code","summary":"Get QR code","description":"One QR code or hub: where it points, its status, its QR image URL and, for a hub, every button with its id (pass those ids to update_hub to keep each button's tap history).","x-mcp-tool":"get_code","parameters":[{"name":"id","in":"path","required":true,"description":"Id of the code or hub, from GET /v1/codes.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"update_code","summary":"Update QR code","description":"Change a QR code (not a hub; use update_hub): its destination, name or placement, or switch it off and on. A printed code keeps its pixels; only where it leads changes. Switching off shows scanners a \"no longer available\" page and frees a plan slot. Changes reach the printed code within about a minute.","x-mcp-tool":"update_code","parameters":[{"name":"id","in":"path","required":true,"description":"Id of the code or hub, from GET /v1/codes.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"destination_url":{"description":"New destination for every scan.","type":"string","minLength":1,"maxLength":2048},"name":{"type":"string","minLength":1,"maxLength":80,"description":"Internal name, shown only in the dashboard."},"placement":{"type":"string","maxLength":80,"description":"Where the code is placed, e.g. \"front desk\" or \"receipt\". Placement reports group by it."},"active":{"description":"false switches the code off; true switches it back on (needs a free slot).","type":"boolean"}},"additionalProperties":false}}}},"responses":{"200":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/codes/{id}/stats":{"get":{"operationId":"get_scan_stats","summary":"Get scan stats","description":"Scans of one QR code or hub, or of the whole account when code_id is left out: total and per day, plus where they came from (scan or a tap on a website embed). Standard adds countries, devices, referrers, per-placement totals and bot filtering. Hub button taps are in get_hub_stats.","x-mcp-tool":"get_scan_stats","parameters":[{"name":"id","in":"path","required":true,"description":"Id of the code or hub, from GET /v1/codes.","schema":{"type":"string","format":"uuid"}},{"name":"days","in":"query","required":false,"description":"How many days back. Default 30. Free plan sees at most 30, Standard 90.","schema":{"type":"integer","minimum":1,"maximum":90}},{"name":"include_bots","in":"query","required":false,"description":"Count link previews and scanners too. Default true. Only Standard can exclude them.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/stats":{"get":{"operationId":"get_account_scan_stats","summary":"Get scan stats","description":"Scans of one QR code or hub, or of the whole account when code_id is left out: total and per day, plus where they came from (scan or a tap on a website embed). Standard adds countries, devices, referrers, per-placement totals and bot filtering. Hub button taps are in get_hub_stats.","x-mcp-tool":"get_scan_stats","parameters":[{"name":"days","in":"query","required":false,"description":"How many days back. Default 30. Free plan sees at most 30, Standard 90.","schema":{"type":"integer","minimum":1,"maximum":90}},{"name":"include_bots","in":"query","required":false,"description":"Count link previews and scanners too. Default true. Only Standard can exclude them.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/hubs":{"post":{"operationId":"create_hub","summary":"Create hub","description":"Create a hub: one printed QR code that opens a page of buttons, e.g. Venmo, PayPal and Zelle on a payment hub, or Google and Yelp on a review hub. Types: payment, review, tip, donation. Each button is tracked separately. Hubs have their own plan limit (free: one hub in total).","x-mcp-tool":"create_hub","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["payment","review","tip","donation"],"description":"What the hub is for."},"name":{"type":"string","minLength":1,"maxLength":80,"description":"Internal name, shown only in the dashboard."},"title":{"description":"Heading on the hub page, e.g. the business name.","type":"string","maxLength":80},"placement":{"type":"string","maxLength":80,"description":"Where the code is placed, e.g. \"front desk\" or \"receipt\". Placement reports group by it."},"links":{"minItems":1,"maxItems":10,"type":"array","items":{"type":"object","properties":{"provider":{"type":"string","description":"Provider key. Allowed per hub type: payment: paypal, venmo, cashapp, zelle, stripe, square, gofundme; review: google, facebook, trustpilot, yelp, bbb, tripadvisor, angi, g2, capterra; tip: paypal, venmo, cashapp, zelle, stripe, square, gofundme; donation: paypal, venmo, cashapp, zelle, stripe, square, gofundme. \"custom\" (any link) needs the Standard plan."},"label":{"type":"string","minLength":1,"maxLength":80,"description":"Button text, e.g. \"Pay with Venmo\"."},"url":{"type":"string","minLength":1,"maxLength":2048,"description":"Where the button goes: the business's own profile or payment link."}},"required":["provider","label","url"]},"description":"The buttons, in display order. Free plan: up to 2."}},"required":["type","name","links"],"additionalProperties":false}}}},"responses":{"201":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/hubs/{id}":{"patch":{"operationId":"update_hub","summary":"Update hub","description":"Change a hub: its buttons, page title and description, name or placement, the \"Powered by Scanlo\" footer (Standard only), or switch it off and on. Its type (payment, review, tip, donation) is fixed: for another type, create a new hub. links replaces the whole set: call get_code first and pass each kept button's id, or its tap history starts again. Changes reach the printed code within about a minute.","x-mcp-tool":"update_hub","parameters":[{"name":"id","in":"path","required":true,"description":"Id of the hub, from GET /v1/codes.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"links":{"description":"The full new set of buttons, in display order. Buttons left out are removed.","minItems":1,"maxItems":10,"type":"array","items":{"type":"object","properties":{"provider":{"type":"string","description":"Provider key. Allowed per hub type: payment: paypal, venmo, cashapp, zelle, stripe, square, gofundme; review: google, facebook, trustpilot, yelp, bbb, tripadvisor, angi, g2, capterra; tip: paypal, venmo, cashapp, zelle, stripe, square, gofundme; donation: paypal, venmo, cashapp, zelle, stripe, square, gofundme. \"custom\" (any link) needs the Standard plan."},"label":{"type":"string","minLength":1,"maxLength":80,"description":"Button text, e.g. \"Pay with Venmo\"."},"url":{"type":"string","minLength":1,"maxLength":2048,"description":"Where the button goes: the business's own profile or payment link."},"id":{"description":"Id of an existing button to keep.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["provider","label","url"]}},"title":{"description":"Heading on the hub page.","type":"string","maxLength":80},"description":{"description":"Text under the heading (Standard only).","type":"string","maxLength":300},"show_scanlo_branding":{"description":"The \"Powered by Scanlo\" footer. Only Standard can turn it off.","type":"boolean"},"name":{"type":"string","minLength":1,"maxLength":80,"description":"Internal name, shown only in the dashboard."},"placement":{"type":"string","maxLength":80,"description":"Where the code is placed, e.g. \"front desk\" or \"receipt\". Placement reports group by it."},"active":{"description":"false switches the hub off; true switches it back on (needs a free slot).","type":"boolean"}},"additionalProperties":false}}}},"responses":{"200":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/hubs/{id}/stats":{"get":{"operationId":"get_hub_stats","summary":"Get hub stats","description":"For one hub: how many people scanned it and how many tapped a button, so you can see how many scanners went on to pay or review. Standard adds the taps per button (which payment app wins).","x-mcp-tool":"get_hub_stats","parameters":[{"name":"id","in":"path","required":true,"description":"Id of the hub, from GET /v1/codes.","schema":{"type":"string","format":"uuid"}},{"name":"days","in":"query","required":false,"description":"How many days back. Default 30. Free plan sees at most 30, Standard 90.","schema":{"type":"integer","minimum":1,"maximum":90}},{"name":"include_bots","in":"query","required":false,"description":"Count link previews and scanners too. Default true. Only Standard can exclude them.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"The same data the MCP tool returns as structured content.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}}},"components":{"securitySchemes":{"oauth2":{"type":"oauth2","description":"OAuth 2.1 authorization code with PKCE (S256). Register a client by dynamic client registration; discovery starts at https://api.scanlo.io/.well-known/oauth-protected-resource. Access tokens last about an hour; use the refresh token.","flows":{"authorizationCode":{"authorizationUrl":"https://ssufyamqezdaqfjplgoe.supabase.co/auth/v1/oauth/authorize","tokenUrl":"https://ssufyamqezdaqfjplgoe.supabase.co/auth/v1/oauth/token","refreshUrl":"https://ssufyamqezdaqfjplgoe.supabase.co/auth/v1/oauth/token","scopes":{}}}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"What went wrong, written to be shown to a person."}},"required":["error"]}},"responses":{"BadRequest":{"description":"Invalid arguments, or a destination that failed validation or Safe Browsing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing, expired or revoked token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"A plan limit, a frozen or disabled code, or a Standard-only setting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No code or hub with that id on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"More than 10 codes and hubs created in the last hour.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}