# Connect your AI to FranchiseSEE Connect Claude, ChatGPT or another AI app to FranchiseSEE and ask about your stores in plain language: "How did my stores do on sales last week?", "Which locations are behind on labor?", "What sold best at lunch?". The app looks the answer up in FranchiseSEE and replies with your own numbers. It sees exactly what your FranchiseSEE login sees, and nothing more. FranchiseSEE connects through the **Model Context Protocol (MCP)**, the open standard AI apps use to reach outside data. > [!NOTE] > FranchiseSEE's AI connections are in beta. Tools, their inputs and their answers may change > without notice, and the setup steps may change as AI apps change their menus. ## Before you start - **Signing in needs AI connections on for your brand.** If you don't see **Connect your AI** in the FranchiseSEE menu, they aren't on yet. Ask your HQ admin or FranchiseSEE support. HQ API tokens work either way. - **Franchisees** can connect only while HQ allows it, by signing in and, separately, with personal tokens. See [Brand switches for HQ admins](https://franchisesee.com/docs/ai/data-scope-and-permissions.md#brand-switches-for-hq-admins). - The **Connect your AI** page inside FranchiseSEE shows the sign-in steps with your connection link ready to copy, plus sample questions to try. ## Which way should I connect? There are two ways in. Most people should sign in. | You want to | Connect | |-------------|---------| | Ask questions in Claude or ChatGPT, set up in a few minutes | [Sign in](https://franchisesee.com/docs/ai/sign-in.md) | | Connect without help from IT, with nothing to install | [Sign in](https://franchisesee.com/docs/ai/sign-in.md) | | Use Claude Code, Cursor or Codex for reporting | [Sign in](https://franchisesee.com/docs/ai/sign-in.md) | | Create or change locations, districts or users from an AI app | [API token](https://franchisesee.com/docs/ai/api-tokens.md) | | Use an app that can't sign in, such as GitHub Copilot | [API token](https://franchisesee.com/docs/ai/api-tokens.md) | | Run a script or a CI job | [API token](https://franchisesee.com/docs/ai/api-tokens.md), or the [REST API](https://franchisesee.com/docs/api/getting-started.md) | | Roll the connection out to a team from a config file IT manages | [API token](https://franchisesee.com/docs/ai/api-tokens.md) | ## How the two compare | | Sign in | API token | |---|---------|-----------| | Setup time | A few minutes | A few minutes, plus editing a config file | | Who sets it up | Anyone with a FranchiseSEE login (franchisees while HQ allows it) | HQ users with *Manage API tokens*; franchisees only while HQ allows it | | Where it works | Web, desktop and phone apps (claude.ai, ChatGPT) and coding tools | Apps that take a config file or header: Copilot, Claude Code, Claude Desktop, scripts | | Servers it reaches | Reporting | Reporting, and admin for HQ only | | Read or write | Always read-only | Read-only, or read and write for HQ | | Expiry | Stays connected until someone disconnects it | HQ tokens can be set never to expire; franchisee tokens expire after 30 days, 90 days or a year | | How to turn it off | **User settings**, then **Connected apps** | **User settings**, then **API tokens** | - **[Connect by signing in →](https://franchisesee.com/docs/ai/sign-in.md)** Claude, ChatGPT and coding tools, with your usual FranchiseSEE login. - **[Connect with an API token →](https://franchisesee.com/docs/ai/api-tokens.md)** The admin server, apps that can't sign in, scripts and IT-managed setups. ## The two servers FranchiseSEE runs two MCP servers, each at its own address: - **Reporting**, `https://app.franchisesee.com/mcp/reporting`: leaderboards, trends, labor, menu items, ratings, goals and more. Read-only. Connect it by signing in or with a token. - **Admin**, `https://app.franchisesee.com/mcp/admin`: create, update and delete locations, districts, categories, location groups and users. API tokens only, and HQ accounts only: it refuses franchisee tokens. The [MCP tool reference](https://franchisesee.com/docs/ai/tools) lists every tool on both servers, with its inputs. ## Next steps - See what a connection can see in [Data scope and permissions](https://franchisesee.com/docs/ai/data-scope-and-permissions.md). - Try the [example prompts](https://franchisesee.com/docs/ai/example-prompts.md). - Something not working? See [Troubleshooting](https://franchisesee.com/docs/ai/troubleshooting.md). --- # Connect by signing in The quickest way to connect. You add FranchiseSEE to your AI app, sign in with your usual FranchiseSEE login, and start asking questions. There is nothing to install and no token to copy, and you don't need help from IT. ## Pick your app | You use | Follow | |---------|--------| | Claude: claude.ai, the desktop app or the phone app | [Claude](https://franchisesee.com/docs/ai/claude.md) | | ChatGPT | [ChatGPT](https://franchisesee.com/docs/ai/chatgpt.md) | | Claude Code, Cursor, Codex or another coding tool | [Developer tools](https://franchisesee.com/docs/ai/developer-tools.md) | Every app connects to the same address: ```text https://app.franchisesee.com/mcp/reporting ``` ## What happens when you sign in 1. You give your AI app the address above. 2. The app opens FranchiseSEE's sign-in page in your browser. You sign in as you always do, including through your company's single sign-on if your brand uses it. 3. FranchiseSEE hands the app a connection tied to your account. The app never sees your password. The connection stays in place until you or HQ disconnect it. Your AI app renews it in the background, so you don't sign in again each time. > [!WARNING] > If your browser is already signed in to FranchiseSEE as someone else, sign out first. Otherwise > the connection lands on the other account, and FranchiseSEE refuses it. ## What a signed-in app can do - **Read-only, always.** A signed-in app can look things up. It can't change settings, goals, locations, users or anything else, whatever your account can do in the web app. - **Reporting only.** Signing in reaches the reporting server. The admin server, which makes changes, takes an [API token](https://franchisesee.com/docs/ai/api-tokens.md) instead. - **Your stores only.** The app sees what your login sees: the whole brand for HQ, your own stores for a franchisee. See [Data scope and permissions](https://franchisesee.com/docs/ai/data-scope-and-permissions.md). The one thing a connection may write is a `report-tool-issue` note, when a tool couldn't answer your question. It holds no store data and changes nothing you can see. ## Franchisees Franchisees can connect by signing in only while HQ has *Let franchisees connect AI apps* turned on. If HQ turns it off, every app a franchisee connected by signing in is disconnected. See [Brand switches for HQ admins](https://franchisesee.com/docs/ai/data-scope-and-permissions.md#brand-switches-for-hq-admins). ## See and disconnect your apps Open the profile menu, choose **User settings**, then **Connected apps**. You see every app connected to your account and the latest reports each one pulled. Disconnect any app there, at any time; it stops reading FranchiseSEE straight away. Removing FranchiseSEE inside the AI app works too. ## Something wrong? See [Troubleshooting: signing in](https://franchisesee.com/docs/ai/troubleshooting.md#signing-in). --- # Connect Claude The FranchiseSEE connector lets Claude answer questions about your franchise from your own FranchiseSEE data: sales and trends, leaderboards, labor, menu items, review ratings and audits, goals, competitions and risk flags. Ask in plain language and Claude looks the numbers up, for one store or across the brand. - **Read-only.** Claude can look things up. It can't change your stores, users or settings in FranchiseSEE. - **Your data only.** Claude sees exactly what your FranchiseSEE login sees: the whole brand for an HQ account, your own stores for a franchisee. - **Nothing to install.** FranchiseSEE is listed in Claude's connector directory. Add it once at [claude.ai](https://claude.ai) or in the Claude desktop app, and it works in the Claude phone app too. > [!NOTE] > FranchiseSEE's AI connections are in beta. Tools and their answers may change, and the setup > steps may change as Claude changes its menus. ## Before you start - **A FranchiseSEE account** on a brand that has AI connections turned on. If you don't see **Connect your AI** in the FranchiseSEE menu, ask your HQ admin. Franchisees can connect only while HQ allows it. - **On a Claude Team or Enterprise plan,** your organization's Claude owner chooses which connectors members can add. If FranchiseSEE doesn't appear for you, ask them to allow it. ## Steps 1. In Claude, open **Settings**, then **Customize**, then **Connectors**. Choose **Discover**. 2. Search for `FranchiseSEE`, then choose **+** next to it. It's listed by FranchiseSEE LLC, with the blue gauge logo. 3. FranchiseSEE's sign-in opens. Sign in with your usual FranchiseSEE login. 4. Start a new chat and ask a question, such as "How did my stores do on net sales last week?" If it answers with your numbers, you're connected. The **Connect your AI** page inside FranchiseSEE shows the same steps. ### Add it as a custom connector instead If you can't add FranchiseSEE from **Discover**, add it yourself. This connects to the same server, with the same sign-in: 1. On the same **Connectors** page, choose **Add custom connector**. 2. Type `FranchiseSEE` as the name, and paste this link as the URL: ```text https://app.franchisesee.com/mcp/reporting ``` 3. Don't change anything else. Choose **Add**, then sign in with your FranchiseSEE login. > [!WARNING] > If your browser is already signed in to FranchiseSEE as someone else, sign out first. Otherwise > the connection lands on the other account, and FranchiseSEE refuses it. ## Try it Claude decides when to look something up in FranchiseSEE; you never need to name a tool. Some questions to start with: > How did net sales trend week by week across our stores from January through March? > Which stores ran the most overtime last week, and what was their labor percentage? > What were our 15 best sellers last month, and what share of checks included each one? > Downtown Austin's score dropped this month. Which metrics are dragging it down? > How does my store stack up against the rest of the brand on net sales this month? If Claude answers from general knowledge instead, say "use FranchiseSEE" in your question, or check that the connector is switched on for the chat in the chat's tools menu. More in [Example prompts](https://franchisesee.com/docs/ai/example-prompts.md). The [MCP tool reference](https://franchisesee.com/docs/ai/tools) lists every tool the connector offers. ## What Claude can see and do The connector reaches FranchiseSEE's reporting server. Every tool on it only reads, except `report-tool-issue`, which files a feedback note for FranchiseSEE's team when a tool can't answer and changes no store data. A signed-in connection can never create, change or delete anything, whatever your account can do in the web app. An HQ account sees every location, with named rankings. A franchisee sees their own stores in full, and the rest of the brand only as group figures, such as a network average or their rank, never another store's name or numbers. See [Data scope and permissions](https://franchisesee.com/docs/ai/data-scope-and-permissions.md). ## Your data and privacy - **Your chats stay in Claude.** FranchiseSEE never sees your conversations. Anthropic's own terms and privacy policy cover them. - **What FranchiseSEE keeps.** For each lookup, FranchiseSEE records which report, stores and dates were read, for 90 days, not your question. When a tool can't answer and Claude files a `report-tool-issue` note, FranchiseSEE keeps that note, with no set time limit, so the team can close the gap. When a tool rejects a request as invalid, FranchiseSEE keeps the rejected inputs for 90 days to improve the tool. - **No password sharing.** You sign in on FranchiseSEE's own page. Claude never sees your password. Read the [FranchiseSEE privacy policy](https://franchisesee.com/privacy) for how FranchiseSEE collects, uses and keeps data, and the [terms of service](https://franchisesee.com/terms). ## Disconnect Remove the connector in Claude under **Settings**, then **Customize**, then **Connectors**. You can also disconnect it from FranchiseSEE: open the profile menu, choose **User settings**, then **Connected apps**, and disconnect Claude there. Either one stops Claude reading FranchiseSEE straight away. ## Troubleshooting - **FranchiseSEE isn't in Discover, or + is missing.** On a Team or Enterprise plan, your organization's Claude owner controls connectors; ask them to allow FranchiseSEE. - **The sign-in finishes, but Claude says access was refused.** Your browser was probably signed in to FranchiseSEE as someone else. Sign out of FranchiseSEE, then connect again. If the message says AI connections are not turned on, ask your HQ admin to turn them on. - **A question about another store is refused.** Franchisee accounts read only their own stores. Ask how you compare with the network or a peer group instead. More fixes in [Troubleshooting](https://franchisesee.com/docs/ai/troubleshooting.md). ## Support Email [support@franchisesee.com](mailto:support@franchisesee.com) with the question you asked and the message you saw. --- # Connect ChatGPT Set this up once at [chatgpt.com](https://chatgpt.com) on a computer. You sign in with your usual FranchiseSEE login, and there is nothing to install. The connection is read-only. See [Connect by signing in](https://franchisesee.com/docs/ai/sign-in.md) for what a signed-in app can see and do. ## Steps 1. In ChatGPT, open **Settings**, then **Plugins**. Choose **Browse Directory**, then **Add custom MCP server**. 2. Type `FranchiseSEE` as the name. 3. Under **Connection**, choose **Server URL** and paste this link: ```text https://app.franchisesee.com/mcp/reporting ``` 4. Under **Authentication**, choose **OAuth**. Leave *Advanced OAuth settings* alone. 5. Tick **I understand and want to continue**, then choose **Create as a plugin**. ChatGPT shows this warning for every app you add yourself. That's expected. 6. FranchiseSEE's sign-in opens. Sign in with your usual FranchiseSEE login. 7. In a chat, type `@`, pick **FranchiseSEE**, and ask a question, such as "Which of my stores had the highest labor percentage last month?" If it answers with your numbers, you're connected. > [!TIP] > On a ChatGPT workspace plan, your workspace admin may control which custom apps members can add. > If **Add custom MCP server** is missing, ask them. ## Using it Mention FranchiseSEE with `@` to have ChatGPT look your data up. Ask in plain language; you never need to name a tool. See [Example prompts](https://franchisesee.com/docs/ai/example-prompts.md) for questions to start with. ## Disconnect Remove the plugin in ChatGPT's **Settings**, or disconnect ChatGPT from FranchiseSEE's [Connected apps](https://franchisesee.com/docs/ai/sign-in.md#see-and-disconnect-your-apps). ## Something wrong? See [Troubleshooting: signing in](https://franchisesee.com/docs/ai/troubleshooting.md#signing-in). --- # Connect developer tools Coding tools connect to the same reporting server as Claude and ChatGPT, using the same [sign-in](https://franchisesee.com/docs/ai/sign-in.md). The connection is read-only. ```text https://app.franchisesee.com/mcp/reporting ``` > [!WARNING] > If your browser is already signed in to FranchiseSEE as someone else, sign out first. Otherwise > the connection lands on the other account, and FranchiseSEE refuses it. ## Claude Code 1. In a terminal, run: ```bash claude mcp add --transport http franchisesee https://app.franchisesee.com/mcp/reporting ``` 2. Start Claude Code, type `/mcp`, choose **franchisesee**, then **Authenticate**. 3. Your browser opens FranchiseSEE's sign-in. Sign in with your usual FranchiseSEE login. Add `--scope user` to the first command to make the server available in every project rather than just the current one. ## Cursor Add the server to `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (one project): ```json { "mcpServers": { "franchisesee": { "url": "https://app.franchisesee.com/mcp/reporting" } } } ``` Open Cursor's MCP settings, where FranchiseSEE shows as needing sign-in. Choose to connect, and sign in with your usual FranchiseSEE login in the browser window that opens. ## Codex Add the server, then sign in: ```bash codex mcp add franchisesee --url https://app.franchisesee.com/mcp/reporting codex mcp login franchisesee ``` Your browser opens FranchiseSEE's sign-in. Sign in with your usual FranchiseSEE login. ## Other tools Most tools that support remote MCP servers over **Streamable HTTP** with OAuth connect the same way: give them the reporting URL and complete the sign-in they start. Your tool's own MCP documentation says where the URL goes. ## When to use a token instead A signed-in coding tool reads reports only. Use an [API token](https://franchisesee.com/docs/ai/api-tokens.md) to manage locations and users through the admin server, for a tool that can't sign in (such as GitHub Copilot), or for a script that runs on its own. Setup for each is in [Token clients](https://franchisesee.com/docs/ai/token-clients.md). ## Something wrong? See [Troubleshooting: signing in](https://franchisesee.com/docs/ai/troubleshooting.md#signing-in). --- # Connect with an API token A personal API token is a long secret string that you paste into an app's configuration. The app sends it with every request, and FranchiseSEE treats the request as coming from you. Tokens are mainly for HQ and IT setups. For everyday questions in Claude or ChatGPT, [signing in](https://franchisesee.com/docs/ai/sign-in.md) is quicker. ## When to choose a token - **Managing locations and users.** The admin server, which creates, updates and deletes locations, districts, categories, location groups and users, accepts tokens only. - **Apps that can't sign in.** GitHub Copilot, Claude Desktop through `mcp-remote`, and any client that takes a header rather than opening a sign-in page. - **Setups IT manages.** A config file rolled out to a team, or a repository's Copilot coding agent, holds a token rather than one person's sign-in. - **Scripts and CI.** A job that runs on its own. For these, the [REST API](#for-scripts-prefer-the-rest-api) is usually the better fit. ## Create a token 1. Sign in to FranchiseSEE. 2. Open the profile menu, choose **User settings**, then **API tokens**. 3. Name the token after the app or job that will use it, such as "Copilot – reporting". 4. Choose its permissions: - `read` is all the reporting server needs. - For the admin server, add `create`, `update` or `delete` for the changes you want it to make. 5. Choose how long it lasts: 30 days, 90 days, a year, or (HQ only) never. 6. Create it, and copy the token straight away. It is shown only once. A token never does more than your account can: its access is the overlap of your account's permissions and the token's. See [Token permissions](https://franchisesee.com/docs/api/getting-started.md#token-permissions). Then set up your app: [Token clients](https://franchisesee.com/docs/ai/token-clients.md) has the configuration for GitHub Copilot, Claude Code, Claude Desktop and other clients. ### Who can create tokens - **HQ (franchisor) accounts** with the *Manage API tokens* permission create tokens with any permissions and any lifetime, including tokens that never expire, for integrations and CI that run unattended. - **Franchisee accounts** create tokens only while HQ has *Let franchisees create personal tokens* turned on. A franchisee token is always read-only, always expires after 30 days, 90 days or a year, and works with the reporting server and the REST API but not the admin server. If HQ turns the switch off, every franchisee token is deleted and stops working. See [Brand switches for HQ admins](https://franchisesee.com/docs/ai/data-scope-and-permissions.md#brand-switches-for-hq-admins). ## The admin server ```text https://app.franchisesee.com/mcp/admin ``` The admin server is for HQ accounts: it refuses a franchisee token before any tool runs. It makes changes, so each one needs the matching permission on your account as well as a token with `create`, `update` or `delete`: | Account permission | Lets you | |--------------------|----------| | Manage Locations | Create, update and delete locations, districts, categories and location groups; bulk district assignment; replace a location's groups | | Manage Users | Update and delete users; add or replace users' location groups; bulk user-to-group assignment | Without the permission, the tool is refused and your AI app reports that the action wasn't authorized. The admin server never accepts a sign-in. The reporting server takes the same tokens, with `read`: ```text https://app.franchisesee.com/mcp/reporting ``` The [MCP tool reference](https://franchisesee.com/docs/ai/tools) lists every tool on both servers. ## Keep a token safe > [!WARNING] > Anyone who has a token can read your FranchiseSEE data as you, and an HQ token with write > permissions can change it. Never commit a token to a repository or paste it into a shared file. - Give each app or job its own token with only the permissions it needs, so you can revoke one without breaking the others. - Keep tokens in your app's secret storage, an environment variable or a secrets manager. The [Token clients](https://franchisesee.com/docs/ai/token-clients.md) examples keep the token out of the config file where the app allows it. - Give an unattended job a token that expires, and replace it before it does, unless it truly must never stop. ## Revoke a token Open the profile menu, choose **User settings**, then **API tokens**, and delete the token. Every app using it stops working at once. Delete any token you no longer use. ## For scripts, prefer the REST API For a script that reads or changes data on a schedule, the [REST API](https://franchisesee.com/docs/api/getting-started.md) is usually a better fit than MCP: plain HTTP requests with stable JSON answers. It takes the same tokens. ## Something wrong? See [Troubleshooting: API tokens](https://franchisesee.com/docs/ai/troubleshooting.md#api-tokens). --- # Token clients Setup for apps that connect with a personal API token. Create the token first: see [Connect with an API token](https://franchisesee.com/docs/ai/api-tokens.md#create-a-token) for permissions, lifetimes and keeping it safe. In every example below, replace `YOUR_API_TOKEN` with your token and connect whichever servers you need: `https://app.franchisesee.com/mcp/reporting` (read-only reporting) and `https://app.franchisesee.com/mcp/admin` (managing locations and users, HQ only). ## GitHub Copilot Copilot connects to FranchiseSEE directly over HTTP, with no bridge to install. This works in VS Code, Visual Studio, JetBrains IDEs, Xcode, the Copilot CLI and the Copilot coding agent. > [!NOTE] > On Copilot Business or Enterprise, your organization's Copilot administrator must first turn on > the **MCP servers in Copilot** policy. If FranchiseSEE doesn't appear, that policy is the most > likely reason. ### VS Code (Copilot agent mode) Add the servers to `.vscode/mcp.json` in your workspace, or to your user MCP configuration. The `inputs` entry makes VS Code ask for your token once and store it securely, so the token never appears in the file: ```json { "servers": { "franchisesee-reporting": { "type": "http", "url": "https://app.franchisesee.com/mcp/reporting", "headers": { "Authorization": "Bearer ${input:franchisesee-token}" } }, "franchisesee-admin": { "type": "http", "url": "https://app.franchisesee.com/mcp/admin", "headers": { "Authorization": "Bearer ${input:franchisesee-token}" } } }, "inputs": [ { "id": "franchisesee-token", "type": "promptString", "description": "FranchiseSEE API token", "password": true } ] } ``` Start the servers from the MCP view, and the FranchiseSEE tools appear in Copilot's agent mode. The reporting server's [guided prompts](https://franchisesee.com/docs/ai/example-prompts.md#guided-prompts) appear as slash commands, such as `/mcp.franchisesee-reporting.analyze-performance`. Visual Studio, JetBrains IDEs, Xcode and the Copilot CLI take the same entries (`type: "http"`, the URL and the `Authorization` header) in their own MCP configuration files. ### Copilot coding agent (github.com) A repository administrator adds the server under the repository's **Settings**, in **Code, planning, and automation**, then **Copilot**, then **MCP servers**: ```json { "mcpServers": { "franchisesee-reporting": { "type": "http", "url": "https://app.franchisesee.com/mcp/reporting", "headers": { "Authorization": "Bearer $COPILOT_MCP_FRANCHISESEE_TOKEN" }, "tools": ["*"] } } } ``` Store the token as an environment secret named `COPILOT_MCP_FRANCHISESEE_TOKEN` in the repository's `copilot` environment. Secrets that start with `COPILOT_MCP_` reach only MCP servers. The `tools` list is required: `["*"]` allows every tool, or list tool names to allow fewer. - **Use a read-only token, and only the reporting server.** Anyone who can assign work to the coding agent acts with that token. Don't give the coding agent the admin server. - **Firewall.** The coding agent runs behind an outbound firewall. If it can't connect, add `app.franchisesee.com` to the agent's allowlist in the repository's Copilot settings. The coding agent uses tools only; the guided prompts aren't available there. ## Claude Code with a token Sign-in is simpler for the reporting server (see [Developer tools](https://franchisesee.com/docs/ai/developer-tools.md#claude-code)). For the admin server, pass the token as a header: ```bash claude mcp add --transport http franchisesee-admin https://app.franchisesee.com/mcp/admin \ --header "Authorization: Bearer YOUR_API_TOKEN" ``` ## Claude Desktop with mcp-remote Claude Desktop connects by signing in (see [Claude](https://franchisesee.com/docs/ai/claude.md)). To use a token instead, for example to reach the admin server, bridge the server with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote). This needs Node.js; `npx` downloads the bridge on first use. Edit Claude Desktop's configuration file: - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "franchisesee-admin": { "command": "npx", "args": [ "mcp-remote", "https://app.franchisesee.com/mcp/admin", "--header", "Authorization: Bearer YOUR_API_TOKEN" ], "env": { "PATH": "PASTE_YOUR_FULL_PATH_HERE" } } } } ``` ### Setting `env.PATH` This is the step that trips people up. Claude Desktop doesn't start from your terminal, so it doesn't inherit your shell's `PATH`. The bridge needs `PATH` to find two things, and both must be there: 1. **`npx` and `node`**: the folder Node.js was installed into. 2. **System tools**: `sh` on macOS and Linux, the system folders on Windows. `npx` runs them, and without them the server disconnects at once. On macOS and Linux the error is `npm error spawn sh ENOENT`. A `PATH` holding only the Node folder is the usual cause. The simplest value that has both is your terminal's own `PATH`. Leave `command` as plain `"npx"`; Claude Desktop finds it through the `PATH` you give it. **macOS and Linux.** Run this in Terminal, copy the whole line it prints, and paste it as the `env.PATH` value: ```sh echo "$(dirname "$(command -v npx)"):$PATH" ``` The value is a list separated by colons. Folders with spaces in them, such as `Application Support`, need no quoting. **Windows.** Run this in Command Prompt (`cmd.exe`), not PowerShell, and paste the result: ```bat echo %PATH% ``` The value is a list separated by semicolons. In the JSON file, backslashes must be doubled (`C:\\Program Files\\nodejs`); if Claude Desktop reports a JSON error, that's the likely cause. Save the file and restart Claude Desktop. FranchiseSEE appears among its connected servers. ## Other MCP clients Any client that supports the **Streamable HTTP** transport can connect directly by sending the token with each request: ```text POST https://app.franchisesee.com/mcp/reporting Authorization: Bearer YOUR_API_TOKEN ``` ## Something wrong? See [Troubleshooting: API tokens](https://franchisesee.com/docs/ai/troubleshooting.md#api-tokens). --- # Data scope and permissions A connected AI app sees exactly what your FranchiseSEE login sees, and nothing more. The address you connect to never grants extra access: connecting the admin server doesn't let you make changes your account can't make, and the admin server is for HQ accounts only. ## What the app can see ### HQ (franchisor) accounts An HQ account sees the **whole brand**: every location, with named rankings, network averages and every store's figures. ### Franchisee accounts A franchisee sees **their own stores in full**: every metric, trend and report, for each store assigned to them. When a tool says "network", it means *your* stores. For the rest of the brand, a franchisee sees **group figures only, never another store**: - **Where you stand.** `get-network-leaderboard-summary` gives your store's rank ("14th of 115"), percentile, the network mean and median, and how many stores improved or declined. - **One peer group at a time.** The `compare_to` option on `get-network-leaderboard-summary`, `get-network-metric-summary` and `get-metric-trendline` compares you with one group: the whole brand, a district, a state or a store format, even one your stores aren't in. You get the group's count, mean, median and quartiles, and where your store sits among them. - **Whole districts.** `whole_districts` on `compare-districts` values each district over all of its stores, not just yours. - **Menu benchmarks.** The item tools give the network's average check penetration and attach rates, naming no store. A group figure is shown only when **at least five stores you don't run** have data in it, and is withheld when it could be subtracted from other figures to reveal a smaller group. A withheld answer says so; choose a larger group. No tool ever gives a franchisee another store's name or numbers. Named, store-by-store rankings live on the FranchiseSEE leaderboard in the web app. ### How tools differ by account Every reporting tool works for both kinds of account. These answer differently: | Tool | HQ account | Franchisee account | |------|------------|--------------------| | `get-leaderboard` | The whole brand, ranked | Only your stores. For network standing use `get-network-leaderboard-summary` | | `get-network-metric-summary` | Whole-brand total, mean and median | Your stores' aggregate, plus one peer group with `compare_to` | | `get-network-leaderboard-summary` | Standing for one named store | Your stores' standing against the network, anonymized | | `get-metric-snapshot`, `get-location-performance` | Value and benchmark, plus rank and network mean/median | Value and benchmark only | | `compare-locations` | Any stores | Only your stores | | `compare-districts` | Every district | Districts with your stores, or whole districts with `whole_districts` | | `get-competition-standings` | Every competing store | Your stores' standings | | `get-insights` | Store and network insights | Insights for your stores | | `get-risk-findings` | Every flag, with tiers | Your stores' flags, without tiers | | `get-goal-progress` | Every goal | Your stores, with anonymized network context | Everything else returns the same shape for everyone, computed over the stores you can see. ## What the app can do - **Signed in:** always **read-only**, and the reporting server only, whatever your account can do in the web app. See [Connect by signing in](https://franchisesee.com/docs/ai/sign-in.md#what-a-signed-in-app-can-do). - **With an API token:** what your account can do, limited further by the token's permissions. Changes go through the admin server, which is for HQ accounts only, and need the matching account permission. Franchisee tokens are always read-only and reach the reporting server and the REST API. Through the API, a franchisee's user list shows the same people the web app does: the users at their own location, or only themselves if they reach their stores through location groups. See [Connect with an API token](https://franchisesee.com/docs/ai/api-tokens.md). Apart from changes made through the admin server, the one thing a connection may write is a `report-tool-issue` note: when a tool can't answer, your AI app can tell FranchiseSEE what was missing. It holds no store data and changes nothing you can see. ## Brand switches for HQ admins AI connections are turned on per brand. Once they are on, HQ admins control franchisee access on the **Connect your AI** page, under *For HQ admins*: | Switch | When it's on | Turning it off | |--------|--------------|----------------| | Let franchisees connect AI apps | Franchisees can connect by signing in; their apps see only their own stores, read-only | Disconnects every app a franchisee connected by signing in. Personal tokens aren't affected | | Let franchisees create personal tokens | Franchisees can create read-only, expiring tokens for scripts and token clients | Deletes every token a franchisee made; scripts using them stop working | HQ accounts' own tokens keep working whatever these switches say. ## Staying in control - **See your connected apps.** Under the profile menu, **User settings**, then **Connected apps**, you see every app connected to your account and the latest reports each one pulled. - **Disconnect any app at any time** from the same page. - **What FranchiseSEE keeps.** For each lookup, FranchiseSEE keeps which report, stores and dates were read, for 90 days, not your question. The exception is a `report-tool-issue` note: when your AI app files one, it describes what it was trying to do, and FranchiseSEE keeps that note so the team can close the gap. - **Your chats stay in your AI app.** FranchiseSEE never sees your conversations with Claude or ChatGPT. The AI provider's own privacy terms cover them. --- # Example prompts Ask in plain language. Your AI app picks the FranchiseSEE tools itself; you never need to name one. The [MCP tool reference](https://franchisesee.com/docs/ai/tools) lists every tool, if you want to see what's behind an answer. > [!TIP] > Your brand's own names work: "the North region", "our Express stores", "Store 1042". The app > learns them from FranchiseSEE before it starts. ## Sales and performance > How did net sales trend week by week across our stores from January through March? > Give me a full scorecard for Downtown Austin last month compared with the month before. > Rank our stores on net sales for last week, with the change from the week before. > Compare Downtown Austin, Round Rock and Store 1042 on net sales, labor percentage and score for > last week. > How does labor percentage compare across our regions last month? ## How do I compare? (franchisees) A franchisee's app never names another store, but it can place you against the network or a peer group. See [Franchisee accounts](https://franchisesee.com/docs/ai/data-scope-and-permissions.md#franchisee-accounts). > How does my store stack up against the rest of the brand on net sales this month? How many > stores are up and how many are down? > How do my sales compare with the top quarter of stores in my format? > Show my weekly sales next to the average store in my district since June. If a comparison comes back withheld, the group is too small to show without revealing a store; choose a larger one, such as your state or the whole brand. ## Labor > Build a labor dashboard for the Northeast region for the last 30 days. > Which stores ran the most overtime last week? ## Menu and checks > What were our 15 best sellers last month, and what share of checks included each one? > What sells best at lunch at Downtown Austin? > How is the Chicken Burrito Bowl trending, and which stores under-sell it? > What do guests add to the Chicken Burrito Bowl most often? > Which items grew or dropped off compared with last year? > What's our average check, and how many items are on a typical check? > How are guests paying: cash, card or delivery apps? > What's our sales split by product category, by order channel and by daypart? ## Scores, ratings and audits > Downtown Austin's score dropped this month. Which metrics are dragging it down? The answer breaks the [SEEScore](https://franchisesee.com/docs/concepts/seescore.md) into the metrics behind it, with each one's weight and gap. > How are our review ratings and audit results looking this quarter? ## Goals, competitions, risk and insights > Are we on pace for this period's sales goal? Which stores are behind? > Where do our stores stand in the current competition? > Which stores have discount or $0-check flags open right now, and why? > Show me any anomaly alerts for Downtown Austin that haven't been delivered yet. ## Managing locations and users These need the [admin server](https://franchisesee.com/docs/ai/api-tokens.md#the-admin-server): an HQ API token with write permissions, and the matching account permission. > Create a location called "Downtown Express" at 123 Main St, Springfield, IL 62701. Put it in the > Central district and classify it as Express. > Move Round Rock from its current region to the South region. > Here's a CSV of locations and their new regions. Update them all. The app checks every row and reports success or failure for each. Names, external ids and UUIDs all work. > Give alice@example.com and bob@example.com access to the West Coast location group, and add East > Coast for Bob. Groups are added; existing ones are kept. > Replace all of john@example.com's location groups with just West Region and East Region. This replaces every group. A user has either one direct location or location groups, never both; see [User access model](https://franchisesee.com/docs/sso/user-access-model.md). ## Guided prompts The reporting server includes two guided prompts that walk your AI app through a multi-step analysis. Apps that support MCP prompts list them in their prompt or slash-command menu. | Prompt | What it does | Arguments | |--------|--------------|-----------| | `analyze-performance` | A detailed analysis of one store across its metrics, with benchmarks | `location` (required: name, UUID or external id); `period` (default `last_month`) | | `network-overview` | The whole network: top and bottom performers and trends | `period` (default `last_month`) | ## Referring to stores, districts and users Every tool accepts more than one kind of identifier: | Entity | UUID | External id | Name or email | |--------|------|-------------|---------------| | Location | Yes | Yes | Name | | District | Yes | Yes | Name | | Location category | Yes | Yes | Name | | Location group | Yes | Yes | Name | | User | Yes | No | Email | --- # Troubleshooting ## Signing in ### There's no Connect your AI item in the FranchiseSEE menu AI connections aren't on for your brand yet, or (for a franchisee) HQ hasn't allowed franchisees to connect. Ask your HQ admin. HQ admins can ask FranchiseSEE support to turn AI connections on. ### The sign-in finishes, but the app says access was refused Usually your browser was already signed in to FranchiseSEE as a different person, so the connection went to that account. Sign out of FranchiseSEE in your browser, then connect again and sign in as yourself. If the message says **"AI connections are not turned on for your FranchiseSEE account"**, AI connections are off for your brand, or HQ turned off *Let franchisees connect AI apps*. See [Brand switches](https://franchisesee.com/docs/ai/data-scope-and-permissions.md#brand-switches-for-hq-admins). ### FranchiseSEE isn't in Claude's Discover list, or Add custom MCP server (ChatGPT) is missing On a Claude Team or Enterprise plan, or a ChatGPT workspace plan, your organization's admin controls which connectors members can add. Ask them to allow FranchiseSEE, or to allow custom connectors. ### My signed-in app connected before, and now it can't 1. **Connected apps.** Under the profile menu, **User settings**, then **Connected apps**: if the app was disconnected there, connect it again. 2. **Brand switch.** Turning off *Let franchisees connect AI apps* disconnects every franchisee's signed-in apps. ### The admin server refuses my sign-in The admin server accepts API tokens only, never a sign-in. See [Connect with an API token](https://franchisesee.com/docs/ai/api-tokens.md). ### The admin server refuses my franchisee token The admin server is for HQ accounts and refuses every franchisee token. Connect the token to the reporting server instead. See [The admin server](https://franchisesee.com/docs/ai/api-tokens.md#the-admin-server). ## API tokens ### There's no API tokens page in User settings HQ accounts need the *Manage API tokens* permission. Franchisee accounts get the page only while HQ has *Let franchisees create personal tokens* turned on. ### My token worked before, and now it's refused 1. **Expiry.** Franchisee tokens always expire, and HQ tokens can. Create a new token under **User settings**, then **API tokens**. 2. **Deleted.** Someone deleted the token on the same page. 3. **Brand switch.** Turning off *Let franchisees create personal tokens* deletes every franchisee token. ### Claude Desktop with mcp-remote disconnects straight away Almost always `env.PATH`. Claude Desktop doesn't inherit your terminal's `PATH`, so the bridge can't find `npx` or the system tools it runs. The error `npm error spawn sh ENOENT` means `sh` is missing. Paste your terminal's full `PATH`, as described in [Setting env.PATH](https://franchisesee.com/docs/ai/token-clients.md#setting-envpath), and restart Claude Desktop. ### Claude Desktop reports a JSON error in its configuration On Windows, backslashes in the `PATH` value must be doubled (`C:\\Program Files\\nodejs`). ### FranchiseSEE doesn't appear in GitHub Copilot On Copilot Business or Enterprise, your Copilot administrator must turn on the **MCP servers in Copilot** policy. For the Copilot coding agent, `app.franchisesee.com` must also be on the agent's firewall allowlist. ### A change was refused as not authorized Changes need the matching permission on both the token and your account: *Manage Locations* or *Manage Users*. Franchisee tokens are always read-only, and a signed-in connection can never make changes. See [The admin server](https://franchisesee.com/docs/ai/api-tokens.md#the-admin-server). ## Using the tools ### The app answers from general knowledge instead of my data Say "use FranchiseSEE" in the question, and check that FranchiseSEE is switched on for the chat. In ChatGPT, mention it with `@`. ### A question about another store is refused Franchisee accounts can read only their own stores. For how you compare with other stores, ask for a comparison with the network or a peer group instead; see [Data scope and permissions](https://franchisesee.com/docs/ai/data-scope-and-permissions.md#franchisee-accounts). ### A peer-group comparison came back withheld A group figure needs at least five stores you don't run with data in it. Choose a larger group: your state instead of your district, or the whole brand. ### "Item-level data is not available for this account yet" The item tools (`get-top-items`, `get-item-performance`, `get-item-affinity`, `compare-item-mix`, `get-basket-metrics`) read check-level POS history, which isn't loaded for every brand yet. Use `get-product-mix` for sales by category meanwhile. Item data is rebuilt nightly, so it never includes today, and yesterday arrives the next morning. A `data_from` note means the range you asked for starts before the history does. ### An item question is slow or asks me to retry Item tools read every line of every check and take seconds, not milliseconds. If one reports it is busy, retry, or narrow the date range or pick one store. ### Percentages look wrong Percent metrics come back as ratios: `0.2` means 20%. Most AI apps convert them, but if one shows `0.2%`, ask it to read the value as a ratio. ### A metric isn't available Each brand turns on its own metrics. Ask "which metrics can you see?", and the app lists them with `list-available-metrics`. Ratings and audit scores need a source (Google, Yelp, SMG and so on); compliance is reported by `get-audit-compliance-report`, not on leaderboards. ## Still stuck? When a tool can't do what you asked, your AI app can file a `report-tool-issue` note so FranchiseSEE's team sees the gap. For anything else, email [support@franchisesee.com](mailto:support@franchisesee.com) with the app you're using, the question you asked and the message you saw. --- # Getting started The FranchiseSEE REST API lets your own systems read performance data and manage locations, districts, location groups and users. Every request is authenticated with a bearer token, and every response is JSON. ## Base URL All endpoints live under one host: ```text https://app.franchisesee.com/api ``` Resource endpoints are versioned under `/api/v1` (for example `/api/v1/locations`). Two endpoints sit outside the version prefix: `GET /api/user` and `POST /api/custom-metrics`. ## Create an API token 1. Sign in to FranchiseSEE. 2. Open the profile menu in the top right, choose **User settings**, then choose **API tokens**. 3. Name the token, choose its permissions and how long it lasts, and create it. 4. Copy the token straight away. It is shown only once. Each token belongs to your account and acts as you. Keep it as secret as a password. > [!NOTE] > Who sees **API tokens** depends on your account: > > - **HQ (franchisor) accounts** with the *Manage API tokens* permission can create tokens with any > permission, including tokens that never expire. > - **Franchisee accounts** can create tokens only when their brand has AI connections on and has > turned on *Let franchisees create personal tokens*. A franchisee token is always read-only and always expires (after 30 > days, 90 days or a year). ## Token permissions When you create a token you choose which of four permissions it carries: | Permission | Allows | |------------|--------| | `read` | View resources and run reports: every `GET` endpoint and the reporting MCP server | | `create` | Create new resources (`POST` create endpoints) | | `update` | Change existing resources (`PATCH` and `PUT`, including the bulk and sync endpoints) | | `delete` | Permanently remove resources (`DELETE` endpoints) | `GET /api/user` is the one exception: any authenticated token may call it, whatever its permissions. Use it to confirm which account a token acts as. A token can never do more than your account can. Its effective access is the **overlap** of your own account permissions and the permissions you gave the token: - A full administrator can still create a `read`-only token to hand to a reporting integration. That token can only read, even though the account can write. - Giving a token `create` does nothing if the account that owns it can't create that kind of resource. The token screen shows a live **"What this token will be able to do"** preview before you create a token, and you can change a token's permissions later. ## Make your first request Send the token in the `Authorization` header with the `Bearer` scheme, and ask for JSON: ```bash curl https://app.franchisesee.com/api/user \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` A successful call returns the account the token acts as: ```json { "data": { "id": "6e2b9d41-8c7a-4f3e-b1d5-9a0c2e4f6b81", "first_name": "Jordan", "last_name": "Rivera", "email": "jordan.rivera@example.com", "perspective": "franchisor", "location_id": null, "has_sso_auth": true, "roles": ["franchisor_admin"], "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } ``` Then list your locations: ```bash curl "https://app.franchisesee.com/api/v1/locations?page[size]=25" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` Collections are paginated, and most can be filtered, sorted and expanded with related records. See [Querying collections](https://franchisesee.com/docs/api/querying.md). ## What a token can see A token sees exactly what its owner sees when they sign in: - **HQ accounts** see every location in the brand. - **Franchisee accounts** see only the locations assigned to them. Asking for any other location returns `403 Forbidden`. Data never crosses brands: every token is scoped to its owner's organization. ## Rate limits The `/api/v1` endpoints allow **60 requests per minute** per user. Every token you own shares that allowance, so give busy integrations their own user account if they need their own limit. Going over returns `429 Too Many Requests` with a `Retry-After` header. See [Errors](https://franchisesee.com/docs/api/errors.md#429-too-many-requests). ## Keep tokens safe - Never share a token or commit it to version control. - Keep tokens in environment variables or a secrets manager, not in code. - Give each integration its own token with only the permissions it needs, so you can revoke one without breaking the others. - Rotate tokens regularly, and delete any you no longer use. - Always call the API over HTTPS. --- # Querying collections The resource collections — locations, location groups, districts, location categories and users — share one set of query parameters for pagination, filtering, sorting and including related records. The data endpoints (`/api/v1/data/*`), risk findings and orders take their own parameters, described on each endpoint's reference page. ## Pagination Collections are paginated by page number. | Parameter | Meaning | Default | |-----------|---------|---------| | `page[size]` | Results per page, up to 100 | 30 | | `page[number]` | Which page to return, starting at 1 | 1 | ```bash curl "https://app.franchisesee.com/api/v1/locations?page[size]=25&page[number]=2" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` Every paginated response carries `links` to the neighbouring pages and a `meta` block with the totals: ```json { "data": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "name": "Downtown Austin" } ], "links": { "first": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "last": "https://app.franchisesee.com/api/v1/locations?page[number]=10", "prev": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "next": "https://app.franchisesee.com/api/v1/locations?page[number]=3" }, "meta": { "current_page": 2, "from": 26, "last_page": 10, "path": "https://app.franchisesee.com/api/v1/locations", "per_page": 25, "to": 50, "total": 250 } } ``` To read a whole collection, follow `links.next` until it is `null`. ## Filtering Add `filter[field]=value` to narrow a collection. Several filters combine with AND: ```bash GET /api/v1/districts?filter[district_group_id]=e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90&filter[external_id]=CTX ``` > [!WARNING] > Filters match **part** of a value and ignore case. `filter[external_id]=STR-1` also returns > `STR-10` and `STR-1042`. When you look a record up by its external id or email to change it, > check the exact value in the results before you act on one. To match any of several values, separate them with commas: `filter[external_id]=STR-1042,STR-1043` returns records matching either. ## Sorting Pass `sort` with a field name. Prefix it with `-` for descending order, and separate several fields with commas; later fields break ties in earlier ones: ```bash GET /api/v1/locations?sort=-created_at,name ``` ## Including related records Pass `include` to return related records nested inside each result, instead of fetching them one by one: ```bash GET /api/v1/locations?include=district,locationGroups ``` ```json { "data": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "city": "Austin", "region": "TX", "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX" }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro" } ] } ] } ``` `include` also works on the single-record endpoints, such as `GET /api/v1/locations/{id}`. ## What each collection accepts Each collection allows its own fields. Asking for anything else returns [`400 Bad Request`](https://franchisesee.com/docs/api/errors.md#400-bad-request) naming the values it does accept. | Collection | `filter[...]` | `sort` (default first) | `include` | |------------|---------------|------------------------|-----------| | `/api/v1/locations` | `external_id` | `name`, `created_at`, `updated_at` | `locationCategory`, `district`, `district2`, `locationGroups` | | `/api/v1/location-groups` | `external_id` | `name`, `created_at`, `updated_at` | `locations` | | `/api/v1/districts` | `external_id`, `district_group_id` | `name`, `created_at`, `updated_at` | `districtGroup` | | `/api/v1/location-categories` | `external_id`, `location_category_group_id` | `sort_order`, `name`, `created_at`, `updated_at` | `locationCategoryGroup` | | `/api/v1/users` | `email`, `perspective` | `email`, `first_name`, `last_name`, `created_at`, `updated_at` | `locationGroups`, `location` | ## Putting it together Pagination, filters, sorting and includes combine freely in one request: ```bash GET /api/v1/locations?include=district,locationGroups&filter[external_id]=STR-1042&sort=-created_at&page[size]=20 ``` ## Tips - Ask for the largest page you can use (`page[size]=100`) when you read a whole collection; it means fewer requests against the [rate limit](https://franchisesee.com/docs/api/errors.md#429-too-many-requests). - Include only the relations you need: each one adds work to every row. - Cache reference data such as districts and categories on your side; it changes rarely. --- # Errors Every error is a JSON body with a `message`, and validation errors add an `errors` map. Send `Accept: application/json` on every request so errors always come back as JSON. | Status | Meaning | What to do | |--------|---------|------------| | [400](#400-bad-request) | A filter, sort or include the endpoint doesn't allow | Use one of the values the message lists | | [401](#401-unauthorized) | Missing, invalid or expired token | Check the `Authorization` header, or create a new token | | [403](#403-forbidden) | The token lacks the permission, or the account can't see the record | Use a token with the right permission and scope | | [404](#404-not-found) | No such record in your organization | Check the id | | [409](#409-conflict) | The record can't be changed in its current state | Read the message, which says why | | [422](#422-unprocessable-entity) | The request body or parameters failed validation | Fix the fields named in `errors` | | [429](#429-too-many-requests) | Over the rate limit | Wait for `Retry-After` seconds | | [503](#503-service-unavailable) | Order history is busy | Retry after `Retry-After` seconds | ## 400 Bad Request You asked a collection for a filter, sort or include it doesn't support. The message names the values it does allow: ```json { "message": "Requested filter(s) `status` are not allowed. Allowed filter(s) are `external_id`." } ``` [Querying collections](https://franchisesee.com/docs/api/querying.md#what-each-collection-accepts) lists what each collection accepts. ## 401 Unauthorized The request carried no token, or the token is invalid, expired or deleted: ```json { "message": "Unauthenticated." } ``` Common causes: - The `Authorization` header is missing. - The header doesn't start with `Bearer ` followed by the token. - The token has expired. Franchisee tokens always expire; HQ tokens expire unless they were created to never expire. - The token was deleted, or (for a franchisee token) the brand turned off franchisee tokens. ## 403 Forbidden The token is valid, but it isn't allowed to do this. Either the token lacks the permission the endpoint needs: ```json { "message": "This API token does not have the 'create' ability." } ``` or the account that owns the token isn't allowed to act on this record. A franchisee asking for a location outside their own stores gets a `403`, as does any account without the role permission for the action. See [Token permissions](https://franchisesee.com/docs/api/getting-started.md#token-permissions). ## 404 Not Found No record with that id exists in your organization: ```json { "message": "Record not found." } ``` Records in other organizations are never visible, so they also return `404`. ## 409 Conflict The record exists, but its current state doesn't allow the change. For example, a location can be deleted only once it is pending, or deactivated with no history left to keep: ```json { "message": "Cannot delete an active location. Deactivate it first." } ``` The message always says why. For locations, the `is_deletable` and `deletable_on` fields tell you ahead of time whether, and from when, a delete will succeed. ## 422 Unprocessable Entity A field failed validation. `message` summarizes the first problem, and `errors` lists every problem by field: ```json { "message": "The name field is required.", "errors": { "name": ["The name field is required."] } } ``` ## 429 Too Many Requests The `/api/v1` endpoints allow 60 requests per minute per user, shared by all of that user's tokens. Over that, the API answers `429` until the window resets. The `Retry-After` header says how many seconds to wait. Back off and retry after that many seconds. To stay under the limit, ask for larger pages (`page[size]=100`) and cache data that changes rarely. ## 503 Service Unavailable The orders endpoints read check-level history, which can be briefly busy: ```json { "message": "Order history is busy; try again in a moment." } ``` Retry after the number of seconds in the `Retry-After` header. --- # Onboarding users with enterprise SSO When your company connects its identity provider (IdP) to FranchiseSEE, the people you give access fall into two groups: - **Corporate-email users** sign in with an address on your company's own domain. - **Personal-email users** sign in with any other address, such as a franchisee's own Gmail account. For both, the first step is the same: **grant the user access to FranchiseSEE in your company's IdP.** ## Corporate-email users Corporate-email users get full single sign-on. When they sign in to FranchiseSEE, they are sent to your company's own login page, and come back signed in. There are two ways to set up their permissions: - **Invite them first (recommended).** Invite the user from the FranchiseSEE admin before their first sign-in, and choose their permissions there. They arrive with the right access from their first sign-in. - **Let them sign in first.** A user who signs in without an invitation gets an account with **no permissions**, so they can't see any data yet. FranchiseSEE emails your corporate helpdesk with instructions for assigning their permissions in the FranchiseSEE admin. ## Personal-email users Personal-email users can't use your enterprise SSO directly, because their address isn't on your company's domain. 1. Grant them access in your IdP. 2. Invite them from the FranchiseSEE admin, choosing their permissions. 3. On their first sign-in, they set a FranchiseSEE password. Their FranchiseSEE account is **deleted automatically when you revoke their access in your IdP**. You don't need to remove them in FranchiseSEE separately. ## Choosing permissions Whichever way a user arrives, their access to locations follows the [user access model](https://franchisesee.com/docs/sso/user-access-model.md): an HQ user sees the whole brand, and a franchisee sees either one location or the locations in their location groups. --- # User access model Every FranchiseSEE user has a **perspective** that decides how much of the brand they see, and a **role** that decides what they can do. ## Perspectives | Perspective | Sees | |-------------|------| | HQ (`franchisor`) | Every location in the brand | | Franchisee (`franchisee`) | Only the locations assigned to them | The same rule applies everywhere: in the web app, the [REST API](https://franchisesee.com/docs/api/getting-started.md) and [connected AI apps](https://franchisesee.com/docs/ai/data-scope-and-permissions.md). A token or AI connection never sees more than its user. ## How a franchisee gets locations A franchisee's access comes from **exactly one** of two sources: - **One direct location.** The user is assigned a single location. This suits a single-store operator. - **One or more location groups.** A location group is a named set of locations (for example "Austin Metro"), and the user sees every location in every group they belong to. This suits a multi-store operator. Adding a store to the group gives every member access at once. The two are **mutually exclusive**. A user has a direct location *or* location groups, never both: - Assigning a direct location **clears** the user's location groups. - Assigning location groups **clears** the user's direct location. "Adding" groups keeps the user's other groups, but it still switches them off a direct location. A franchisee with neither a location nor a group sees no store data until one is assigned. ### Setting access by API or AI | To | REST API | Admin MCP tool | |----|----------|----------------| | Give a user one direct location | `PATCH /api/v1/users/{id}` with `location_id` | `update-user` | | Replace all of a user's groups | `PUT /api/v1/users/{id}/location-groups` | `sync-location-groups-for-user` | | Add groups to many users at once | `POST /api/v1/users/bulk-location-groups` | `bulk-add-location-groups-for-users` | | Add groups to one user | | `add-location-groups-for-user` | | Replace a location's groups | `PUT /api/v1/locations/{id}/location-groups` | `sync-location-groups-for-location` | Each needs the *Manage Users* permission (or *Manage Locations* for a location's groups) and a token with the `update` permission. ## Roles Each perspective has two roles: | Role | Perspective | Can | |------|-------------|-----| | User (`franchisor_user`) | HQ | View dashboards, analytics, reports and settings across the brand | | Admin (`franchisor_admin`) | HQ | Everything a User can, plus manage users, locations, competitions, goals, API tokens, SSO and company settings | | User (`franchisee_user`) | Franchisee | View their own stores' dashboards, analytics, risk flags and goals, export orders, propose goals | | Admin (`franchisee_admin`) | Franchisee | The same as a franchisee User | HQ admins can adjust which permissions each role carries in the brand's permission settings. ## With enterprise SSO Users who sign in through your company's identity provider follow the same model. Invite them first to set their perspective, role and locations before their first sign-in; see [Enterprise SSO onboarding](https://franchisesee.com/docs/sso/enterprise-sso-onboarding.md). --- # SEEScore SEEScore™ is a composite performance score ranging from **25 to 200**, calculated for each location in your network. It combines multiple configurable metrics — each with its own weight and performance thresholds — into a single comparable number that reflects the overall health of a location. --- ## Available Metrics The following metrics can be included in your SEEScore™ configuration: ### Sales | Metric | Description | |--------|-------------| | Net Sales | Total revenue generated | | Transactions | Number of customer transactions | | Customers | Number of customers served | | Average Ticket Size | Average revenue per transaction | | Average Sales per Customer | Revenue per customer visit | ### Labor | Metric | Description | Direction | |--------|-------------|-----------| | Variable Labor Cost | Total direct labor spend | Lower is better | | Variable Labor % | Labor as a percentage of sales | Lower is better | | Sales Per Labor Hour (SPLH) | Revenue per labor hour | Higher is better | | Customers Per Labor Hour | Customers served per labor hour | Higher is better | ### Food & Product Cost | Metric | Description | Direction | |--------|-------------|-----------| | Food Cost (COGS) | Total cost of goods sold | Lower is better | | Food Cost % | COGS as a percentage of sales | Lower is better | | Cost Per Meal | COGS per customer | Lower is better | ### People | Metric | Description | Direction | |--------|-------------|-----------| | Managerial Turnover Rate | Leadership staff turnover | Lower is better | | Non-Managerial Turnover Rate | Frontline staff turnover | Lower is better | ### Customer Experience | Metric | Description | |--------|-------------| | Rating | Aggregated customer review score (supports multiple sources) | | Store Audit Score | Operational audit result (supports multiple sources) | ### Compliance | Metric | Description | |--------|-------------| | Compliance | Pass/fail compliance status from a configured source | ### Custom & Competitive These metrics are defined per account rather than being fixed, numbered slots. Each one is configured with its own name, and is identified by a key derived from that name (for example, a "Loyalty Signups" custom metric is keyed `custom:loyalty_signups`, and a "Rival Brand" competition metric is keyed `competition:rival_brand`). | Metric | Description | |--------|-------------| | Custom metrics | Brand-specific KPIs defined per account, each individually named. An account can configure any number of them. | | Competition metrics | Competitive benchmarking metrics defined per account, each named for the competitor or benchmark it tracks. | --- ## Calculation SEEScore™ is calculated in three stages. ### 1. Metric Weights Each metric in the configuration is assigned a **weight from 1 to 5**, which controls how much that metric influences the final score. | Weight | Impact | |--------|--------| | 1 | Very Low | | 2 | Low | | 3 | Moderate | | 4 | High | | 5 | Very High | ### 2. Performance Component Every metric contributes a **performance score** based on two configured thresholds — a lower and an upper threshold — and a per-metric choice of **scoring curve**. The performance multiplier (0–10) is then multiplied by the metric's weight. The scoring curve is chosen per metric on the score configuration page; new metrics use the **smooth curve** by default. The maximum performance points for a metric is `weight × 10` with either curve. #### Smooth curve (default) The performance multiplier **ramps linearly from 0 at the lower threshold to 10 at the upper threshold** — clamped to 0 below and 10 above. This avoids abrupt jumps at the thresholds and differentiates locations that fall within the same range. A location halfway between the thresholds scores 5, a quarter of the way scores 2.5, and so on. #### Step curve (alternative) Instead of a continuous ramp, the thresholds define three fixed tiers: | Tier | Condition | Performance multiplier | |------|-----------|------------------------| | Poor | Value does not meet the lower threshold | 0 | | Adequate | Value is between the two thresholds | 5 | | Excellent | Value exceeds the upper threshold | 10 | **Directionality:** For metrics where lower is better (labor %, food cost %, turnover rates), the threshold comparison is inverted automatically — a sufficiently low value earns full points, and a value above the upper threshold earns zero. Both curves invert the same way. **Compliance:** Treated as binary regardless of curve — compliant earns full points (`10 × weight`); non-compliant earns zero. #### Setting thresholds Thresholds can be entered manually or generated with the **Suggest range** tool, which reads the actual distribution of location values for the metric over a chosen period and proposes a lower and upper threshold from percentiles of that data. You can then fine-tune before applying. The tool offers three splits. They differ in how much of the network lands in the poor/excellent extremes versus inside the band: | Split | Thresholds at | Effect | |-------|---------------|--------| | Quintiles | 20th / 80th percentile | **Forgiving** — only the clear top and bottom 20% reach the extremes; the middle 60% stay within the band. | | Quartiles | 25th / 75th percentile | **Balanced** — the top and bottom 25% reach the extremes; the middle half stays within the band. | | Tertiles | 33rd / 67th percentile | **Strict** — even thirds, pushing far more locations to the poor and excellent extremes. | **Which to choose:** quintiles is the sensible default — it keeps most locations inside the band and flags only the clear standouts and laggards. Move toward quartiles or tertiles when you want stricter grading that spreads more locations to the extremes. This choice matters more with the **smooth curve**: only locations *inside* the band are graded continuously, while anything past a threshold clamps to 0 or 10. A wider split (quintiles) therefore keeps more of the network on the differentiating ramp, whereas a narrow split (tertiles) clamps the majority at the extremes — which makes the smooth curve behave more like the step curve. For smooth metrics, lean toward the wider splits. ### 3. Normalisation After summing earned points and total possible points across all metrics, the raw score is normalised to the 25–200 scale: ``` SEEScore = round(earnedPoints × (175 / totalPossiblePoints)) + 25 ``` - The minimum achievable score is **25** (every metric at Poor — i.e. each value at or below its lower threshold). - The maximum achievable score is **200** (every metric at Excellent — each value at or above its upper threshold). - With the **smooth curve**, scores vary continuously between these bounds: a location's score reflects exactly where its values fall within each band, rather than snapping to a small set of tier-based totals as the step curve does. - **Metrics with no data for a location are excluded** from both earned and possible points — a location is scored only on the metrics it has data for, so a missing value neither helps nor penalizes its score. - A score of **0** indicates no metric data is available for the location at all. --- ## Score Reference | Score Range | Interpretation | |-------------|----------------| | 170 – 200 | Exceptional | | 130 – 169 | Strong | | 90 – 129 | Adequate | | 25 – 89 | Needs Attention | | 0 | No data available | --- # MCP tool reference ## Reporting (`franchisesee-reporting`) Server URL: `https://app.franchisesee.com/mcp/reporting` ### `compare-districts` Compare ONE metric across the districts in a district group — the tenant's own grouping (e.g. "Region" or "Market") — over a date range, ranked best to worst. Each district's value is aggregated across the locations in it (summed for summable metrics like net_sales_amount; otherwise the unweighted typical-location mean, including ratio metrics like labor_percentage), so this answers "compare typical location labor % across my districts/regions" in a single call. Defaults to the tenant's primary district group; pass district_group to choose another (call get-tenant-context for the available group names). A summed value also counts locations that have since closed, for the days they traded in the range (closed_num_locations of them, beyond num_locations); a mean covers open locations only. Franchisee tokens are scoped to their own locations by default, so only districts that contain their locations appear, valued over those locations alone; pass whole_districts=true to compare WHOLE districts instead (every shared-leaderboard store in each, as aggregates only — no store is named), valued over open stores, with includes_your_stores marking theirs; a district with fewer than five stores outside the caller's own, or one that would single out such a district by subtraction from the network, is withheld (value null, withheld true: say so, never estimate it). Percentage metrics are ratios (e.g. 0.2 = 20%). Use list-available-metrics for valid metric keys. - `metric` (string, required): Metric key (e.g., net_sales_amount, labor_percentage, score) - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `district_group` (string | null): Which district grouping to compare across, by the tenant's own name (e.g. "Region", "Market"). Defaults to the primary group. - `source` (string | null): Data source (only for rating and audit_score): google, yelp, store_audit, ecosure, smg, meazureup - `whole_districts` (boolean | null): Franchisee tokens: value each district over all of its stores rather than only the caller's own, to compare their districts with the rest. Franchisor tokens always see whole districts. ### `compare-item-mix` How the menu ITEM mix shifted between a date range and its comparison: benchmark=prior (default; the adjacent range of the same length) or yoy (the same weekdays 52 weeks earlier). Returns `gainers` and `decliners` ranked by the change in net sales, `new_items` (sold now, not before) and `dropped_items` (sold before, not now), each with net_sales, previous_net_sales, change, change_rate, share and previous_share of the item total. `window` names both date ranges. Pass "category" to compare within one business line (group-first). `top` limits each list (default 20, max 50). Pass "daypart" (a name from get-daypart-mix) to compare within one daypart in both windows, each item counted toward the daypart its check was opened in; accounts without dayparts get an error. Top-level items only; service charges excluded. Franchisee tokens are scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `benchmark` (string | null): prior (default) or yoy - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for network-wide. - `category` (string | null): Optional category or group name to compare within. Resolved group-first, then a single category. - `top` (integer | null): Items per list (default 20, max 50) - `daypart` (string | null): Optional daypart (its name or key, e.g. "Breakfast", from get-daypart-mix) to read within: figures count only checks opened in that daypart, and check penetration is over that daypart's checks. Only for accounts with dayparts configured. ### `compare-locations` Compare several locations side-by-side across one or more metrics for a date range. Returns a value per location per metric (null where there is no data). Percentage metrics (labor_percentage, cogs_percentage, turnover) are ratios (e.g. 0.2 = 20%). Identify each location by UUID, external_id, or name; every location must be accessible to your token. Use list-available-metrics for valid metric keys. - `locations` (array, required): Location identifiers (UUID, external_id, or name) to compare - `metrics` (array, required): Metric keys to compare (e.g., net_sales_amount, labor_percentage, score) - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `source` (string | null): Data source (only for rating, audit_score metrics): google, yelp, store_audit, ecosure, smg, meazureup, restaurant365 ### `get-audit-compliance-report` Store audit scores and compliance for a single location or network-wide, in one call — enumerating each audit source (EcoSure, MeazureUp, …) and compliance source (Restaurant365, …) the tenant has enabled. Audit scores are averaged over the date range; compliance is the current compliant share (a ratio, e.g. 0.8 = 80% of locations compliant) and ignores the date range, since compliance is a current-state flag. Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) — applies to audit scores - `end_date` (string, required): End date (YYYY-MM-DD) — applies to audit scores - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. ### `get-basket-metrics` Basket shape for a single location or network-wide over a date range, from check-level POS data: checks, net_sales, avg_check, avg_items_per_check (top-level items, not modifiers), avg_guests_per_check (null when the POS reports no guest counts), and check_size_distribution — the number and share of checks under $5, $5–$10, $10–$20, $20–$50 and $50 and over (by check net sales). Pass group_by (day, week, month) for a `series` of the same figures per period. Franchisee tokens are scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for network-wide. - `group_by` (string | null): Optional series grain: day, week, or month ### `get-channel-mix` Sales and transaction split by order CHANNEL (e.g. delivery, pickup) and order TYPE (e.g. dine-in, takeout) for a single location or network-wide over a date range, each ranked by net sales. Built from the retained `sales_by_channel` and `sales_by_type` digest metadata. For item-level and per-check questions use get-top-items, get-item-performance, compare-item-mix and get-basket-metrics. Returns empty lists if the tenant has not populated channel/type metadata. A network mix covers open locations only, while a network `net_sales_amount` total also counts closed (deactivated) locations for the days they traded, so channel or type sales need not add up to it. Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. ### `get-cogs-report` Cost-of-goods-sold bundle for a single location or network-wide over a date range, in one call: total COGS, COGS percentage (a ratio, e.g. 0.3 = 30%, weighted by sales across the range), and COGS per customer. Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. ### `get-competition-standings` Ranked standings for the tenant's current competition in one call: the competition window (start/end dates), the competition metric, and each location ranked by value, including the extra per-competition value slots that the generic leaderboard omits. What each slot means is competition-specific — read value_labels in the response for the meaning of value, value_2, and value_3. A convenience over get-leaderboard("competition:") — no need to know the competition slug. Franchisee tokens see only their authorized locations; franchisors see the whole tenant. Requires the Competition feature. ### `get-custom-metric-report` Values for this tenant's custom metrics for a single location or network-wide over a date range, in one call — enumerating every custom metric the tenant has configured, with its display label resolved. Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. Pass "metric" to limit the report to one custom metric key (e.g. custom:loyalty_signups). - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. - `metric` (string | null): Optional custom metric key (e.g. custom:loyalty_signups) to report just one metric ### `get-daypart-mix` Sales split by the tenant's DAYPARTS (its named parts of the trading day, e.g. Breakfast, Lunch, Dinner) for a single location or network-wide over a date range, in trading-day order: net sales, transactions, customers and share of net per daypart, each with its window ("11:00 AM – 2:00 PM", or "Varies by day"). "Outside dayparts" holds sales that could not be placed on the clock. Where labor is split too, each daypart adds labor_minutes and sales_per_labor_hour, counted over only the store-days carrying both splits (labor_coverage says how many), so quote SPLH with that coverage and never divide the sales figures by labor_minutes yourself. Daypart labor is fixed when the day is built, so on an older range where jobs were since marked salaried its minutes can exceed get-labor-report hours. Store-days with no daypart split (dayparts set up partway through the range) are left out, not read as zero; coverage says how many store-days of the range the figures cover. Covers open locations only (sales_per_labor_hour is null for Outside dayparts, whose sales and labor are unrelated). Returns a message and no rows when the tenant has no dayparts configured. Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. ### `get-goal-progress` Goals: targets on a metric (net sales budget, labor % ceiling, average check, rating level, …) for a set of stores over a span split into checkpoints (e.g. fiscal periods). Returns each live goal's current checkpoint, the rolled-up actual vs target, the projected finish, plain pace sentences (quote them: "To catch up, average $1,830 a day for the last 11 days"), the status split (ahead / on track / at risk / behind / too early — always mention it beside a roll-up, because an average hides a failing store), and per-store rows worst first. Status is graded on the projected finish against a seasonal curve from last year, never calendar time. Pass goal (name or id) for one goal, location for one store, include_closed for finished goals. Franchisee tokens see only goals shared with them and only their own stores, plus an anonymized network_context line. Requires the Goals feature. - `goal` (string | null): Goal name (partial match) or id — omit for every live goal - `location` (string | null): Location identifier (UUID, external_id, or name) — omit for every store in scope - `include_closed` (boolean | null): Also return goals whose span has finished (graded hit or missed) ### `get-insights` Retrieve AI-generated insights about location or network performance. Filter by location, insight type, tier, or delivery status. - `location` (string | null): Location identifier (UUID, external_id, or name) — omit for network-level insights - `insight_type` (string | null): Filter by type: daily_sales_summary, daily_labor_summary, daily_metric_summary, network_overview, anomaly_alert, score_driver_breakdown, score_improvement_plan - `tier` (integer | null): Filter by tier (1=descriptive, 2=diagnostic, 4=prescriptive) - `undelivered_only` (boolean | null): Only return undelivered insights - `limit` (integer | null): Max results to return (default 10) ### `get-item-affinity` What gets added to one menu ITEM (#880): its modifiers ranked by attach rate — the share of the item's lines that carried the modifier at least once (two bowls on a check, one with avocado = 50%). `kind` is "add-on" for POS systems whose modifiers include paid up-sells (Brink) and "option" where they are mostly $0 choices and combo parts such as a drink flavour (Qu) — word the answer accordingly. Each modifier carries attached_lines, net_sales, priced_lines (lines that were charged for; 0 = a free choice), net_per_attach, stores_attaching and `network_average_attach_rate` (the average store's rate across the whole network, a store that never attached it counting as 0 and stores with under 30 lines of the item left out — anonymised, so franchisee tokens get it too; null for them when fewer than five other stores stand behind it). Pass `modifier` (its POS external id or name) on a network call with a franchisor token for `by_location`: every store that sold the item with its attach_rate, attach_gap (store minus network average, ratio points; negative = under-attaching), low_volume, and gap_net_sales_estimate (an estimate of the modifier sales the missing attaches would have brought; null at or above average and for a free modifier — present it as an estimate, never a forecast). Identify the item by its POS external id (from get-top-items) or its name. Franchisee tokens are scoped to their authorized locations and never receive other stores. - `item` (string, required): The item: its POS external id or its name - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for network-wide. - `modifier` (string | null): Optional: one modifier of the item (POS external id or name) to rank stores by. Franchisor tokens, network calls only. - `top` (integer | null): How many modifiers to return (default 15, max 50) ### `get-item-performance` How one menu ITEM sold over a date range: totals (net_sales, quantity, checks, check_penetration = share of checks that included it, avg_unit_price), a trend by day, week or month, `network_average_penetration` (the average store's check penetration across the whole network, a store that never sold the item counting as 0% — anonymised, so franchisee tokens get it too — null for them when fewer than five other stores traded, since so few would give a store away), and — for franchisor tokens on a network call — `by_location`, every store that traded ranked by the item's net sales with its share_of_location_sales, check_penetration, never_sold, penetration_gap (store minus network average, ratio points; negative = under-selling) and gap_net_sales_estimate (an estimate of the sales the missing checks would have brought at the item's average unit price; null at or above average — present it as an estimate, never a forecast). Pass benchmark=prior (the adjacent range of the same length) or yoy (the same weekdays 52 weeks earlier) for deltas. Identify the item by its POS external id (from get-top-items) or its name; an ambiguous name returns the candidates. Pass "daypart" (a name from get-daypart-mix) to read within one daypart: totals, trend, each store's penetration and the network average all count only checks opened in it, and the five-other-stores floor counts only stores that traded in it; accounts without dayparts get an error. Counts top-level sales of the item, not its use as a modifier. Franchisee tokens are scoped to their authorized locations and never receive other stores. - `item` (string, required): The item: its POS external id or its name - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for network-wide. - `group_by` (string | null): Trend grain: day, week (default), or month - `benchmark` (string | null): none (default), prior, or yoy - `daypart` (string | null): Optional daypart (its name or key, e.g. "Breakfast", from get-daypart-mix) to read within: figures count only checks opened in that daypart, and check penetration is over that daypart's checks. Only for accounts with dayparts configured. ### `get-labor-report` Full labor picture for a single location or network-wide over a date range, in one call: labor cost, labor percentage (a ratio, e.g. 0.2 = 20%), sales-per-labor-hour, customers-per-labor-hour, total labor hours, and overtime hours. Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. For a per-metric time series use get-metric-trendline. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. ### `get-leaderboard` Get a ranked leaderboard for a metric over a time period. Returns all locations ranked by value with optional benchmark comparison (prior period or year-over-year). The previous_total aggregate covers every location that reported in the benchmark window (previous_num_locations of them, including any since closed), while the per-location rows only carry a previous_value for locations ranked in the current window — so summing the rows' previous values will not reproduce previous_total whenever the two counts differ. Totals (total, previous_total) also count closed locations for the days they traded — closed_num_locations and previous_closed_num_locations of them — which never appear as rows; mean and median cover open locations only. With a benchmark, total_change and total_change_rate compare total with previous_total, and num_locations_up / num_locations_down count the rows whose value rose or fell (rows with no previous_value are in neither, and both are null when no row has one; for a lower-is-better metric, up is worse). total_change_rate is not a same-store figure: the two totals cover different store sets (num_locations plus closed_num_locations now, previous_num_locations then), so say so rather than presenting it as like-for-like growth — quote these rather than subtracting totals or counting rows. Use list-available-metrics to see valid metric keys. - `metric` (string, required): Metric key (e.g., net_sales_amount, labor_percentage, score) - `period` (string, required): Time period: current, yesterday, last_week, last_month, last_90_days, ytd, last_tenant_period - `benchmark` (string | null): Benchmark comparison: none (default), prior, yoy - `source` (string | null): Data source (only for rating and audit_score): google, yelp, store_audit, ecosure, smg, meazureup. Compliance is not a leaderboard metric — use get-audit-compliance-report. - `district_name` (string | null): Filter to locations in this district (by name) - `category_name` (string | null): Filter to locations in this category (by name) - `states` (array | null): Filter to locations in these states/regions - `hide_corporate` (boolean | null): Exclude corporate-owned locations ### `get-location-daily-metrics` Get day-by-day operational data for a location over a date range. Returns daily sales, transactions, customers, and labor data for trend analysis. labor_percentage is a ratio (e.g. 0.2 = 20%), consistent with other reporting tools. Identify the location by UUID, external_id, or name. - `location` (string, required): Location identifier (UUID, external_id, or name) - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `include_labor` (boolean | null): Include labor data (default true) ### `get-location-performance` Get a single location's metric value, rank, and optional benchmark comparison. Identify the location by UUID, external_id, or name. - `location` (string, required): Location identifier (UUID, external_id, or name) - `metric` (string, required): Metric key (e.g., net_sales_amount, labor_percentage) - `period` (string, required): Time period: current, yesterday, last_week, last_month, last_90_days, ytd, last_tenant_period - `benchmark` (string | null): Benchmark comparison: none (default), prior, yoy - `source` (string | null): Data source (only for rating and audit_score). Compliance is not a leaderboard metric — use get-audit-compliance-report. ### `get-metric-snapshot` One call returning every enabled metric for a single location over a period — value, rank, network mean/median, and benchmark comparison (prior period by default). Powers a full-location overview without one call per metric. Percentage metrics (labor_percentage, cogs_percentage, turnover) are ratios (e.g. 0.2 = 20%). Source-based metrics (ratings, audits) are excluded — use get-location-performance or get-metric-trendline with a source for those. Identify the location by UUID, external_id, or name. - `location` (string, required): Location identifier (UUID, external_id, or name) - `period` (string, required): Time period: current, yesterday, last_week, last_month, last_90_days, ytd, last_tenant_period - `benchmark` (string | null): Benchmark comparison: prior (default), yoy, none ### `get-metric-trendline` Time series for any metric over a date range — for a single location or network-wide. Group by day (default), week (tenant week-start aware), or month. The response includes range_value: the exact figure for the range — a total or weighted average across it, or for point-in-time metrics the standing as of the end date, per range_aggregation — ALWAYS use it for any whole-range figure instead of summing or averaging the points yourself. Percentage metrics (labor_percentage, cogs_percentage, turnover) are ratios (e.g. 0.2 = 20%). Omit "location" for a network trendline; franchisee tokens are automatically scoped to their authorized locations. Report-based metrics (cogs, retention) return one point per report period regardless of grouping. Competition metrics return a cumulative running-total curve truncated to the competition window — points are standings, never add them together — plus a competition block naming the resolved competition and its window. To set the trend beside other stores, pass compare_to (+ compare_to_name) for ONE group — the whole network, a district, a state or a category — and the response adds comparison_group.points: the TYPICAL STORE in that group per bucket (mean and median of each store's value for the bucket, e.g. one store's weekly sales), on the same buckets — compare a single location's points with it directly (for one store, each point also carries yours and gap = yours − mean, and gap_summary counts the buckets it beat the typical store in and names the closest and widest — quote these rather than reading the points), but divide a multi-location network total by its store count first. A bucket with too few stores outside the caller's own (or one that would single out a smaller group by subtraction) is withheld: say so, never fill it in. At most 26 buckets — group by week or month for longer ranges; not available for competition metrics. Use list-available-metrics for valid metric keys. - `metric` (string, required): Metric key (e.g., net_sales_amount, labor_percentage, score) - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide trendline. - `grouping` (string | null): Time bucket: day (default), week, month - `source` (string | null): Data source (only for rating, audit_score metrics): google, yelp, store_audit, ecosure, smg, meazureup, restaurant365 - `compare_to` (string | null): Compare against the stores in ONE group, as aggregates only (store count, mean, median, quartiles, movement; total for franchisor tokens): network (the whole brand), district (either district group), state, or category (e.g. a store format). Works for franchisee tokens too — the group need not contain their own stores — and is withheld when fewer than 5 stores outside the caller's own stand behind it. No store in the group is ever named. - `compare_to_name` (string | null): The district, state/region or category to compare against. Omit it for a category or state to use the caller's own (e.g. "stores in my format"); a district must be named, and leaving it out returns the caller's own districts to choose from. ### `get-network-leaderboard-summary` Show where the caller's own location(s) stand within the wider network for one metric: their rank ("14th of 115"), their percentile, and how many network locations improved vs. declined against the benchmark. Returns the caller's own row plus anonymous network aggregates only — never another location's name or value; the app's leaderboard is where named per-store numbers live. Unlike get-leaderboard, which scopes a franchisee to their own locations, this ranks them against the full peer board, so it is the tool to use for "how do I compare to the network / am I above average / how many stores are down". Peer board = the same board the location is ranked against in the app (category partitioning and corporate visibility respected). To compare against a different group — "my district", "stores in Texas", "the GC10 format", "the whole brand" — pass compare_to (+ compare_to_name): the response adds comparison_group (that group's store count, mean, median, quartiles, top_quarter_starts_at = the value a store must beat to be in its best quarter, and movement counts) and each location's rank or percentile within it, even when the location is not in that group. A group is withheld when fewer than five stores outside the caller's own stand behind it, or when it would single out a smaller withheld group by subtraction: say so, never estimate it. - `metric` (string, required): Metric key (e.g., net_sales_amount, labor_percentage, score). Use list-available-metrics for the keys this tenant supports. - `period` (string, required): Time period: current, yesterday, last_week, last_month, last_90_days, ytd, last_tenant_period - `benchmark` (string | null): Which comparison the improved/declined counts are measured against: prior (default) or yoy. Not every tenant has a year of history — when yoy resolves to nothing the response says so rather than reporting zero movement. - `source` (string | null): Data source (only for rating and audit_score): google, yelp, store_audit, ecosure, smg, meazureup - `location_name` (string | null): Which of the caller's own locations to report standing for. Optional for a franchisee — omit it to cover every location they operate. Franchisors must name one. - `compare_to` (string | null): Compare against the stores in ONE group, as aggregates only (store count, mean, median, quartiles, movement; total for franchisor tokens): network (the whole brand), district (either district group), state, or category (e.g. a store format). Works for franchisee tokens too — the group need not contain their own stores — and is withheld when fewer than 5 stores outside the caller's own stand behind it. No store in the group is ever named. - `compare_to_name` (string | null): The district, state/region or category to compare against. Omit it for a category or state to use the caller's own (e.g. "stores in my format"); a district must be named, and leaving it out returns the caller's own districts to choose from. ### `get-network-metric-summary` Aggregate one metric across all (or filtered) locations for an arbitrary date range. Returns the network distribution — total (summable metrics only), typical-location mean, median, and the count of locations with data — plus a short ranked preview. The total also counts locations that have since closed, for the days they traded in the range (closed_num_locations of them, beyond num_locations); mean and median cover open locations only. Digest-backed ratio metrics also return weighted_mean, the network treated as one business (sum numerator / sum denominator). Percentage metrics (labor_percentage, cogs_percentage, turnover) are ratios (e.g. 0.2 = 20%). Use list-available-metrics for valid metric keys. Franchisee tokens are automatically scoped to their authorized locations; to compare those against other stores, pass compare_to (+ compare_to_name) for ONE group — the whole network, a district, a state or a category — and the response adds comparison_group: that group's store count, mean, weighted_mean, median, quartiles and top_quarter_starts_at (the value a store must beat to be in its best quarter), plus total for franchisor tokens, never a store row. A group is withheld when fewer than five stores outside the caller's own stand behind it, or when it would single out a smaller withheld group by subtraction: say so, never estimate it. - `metric` (string, required): Metric key (e.g., net_sales_amount, labor_percentage, score) - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `source` (string | null): Data source (only for rating and audit_score): google, yelp, store_audit, ecosure, smg, meazureup. Compliance is not a leaderboard metric — use get-audit-compliance-report. - `district_name` (string | null): Filter to locations in this district (by name) - `category_name` (string | null): Filter to locations in this category (by name) - `states` (array | null): Filter to locations in these states/regions - `hide_corporate` (boolean | null): Exclude corporate-owned locations - `top` (integer | null): How many top-ranked locations to include in the preview (default 5) - `compare_to` (string | null): Compare against the stores in ONE group, as aggregates only (store count, mean, median, quartiles, movement; total for franchisor tokens): network (the whole brand), district (either district group), state, or category (e.g. a store format). Works for franchisee tokens too — the group need not contain their own stores — and is withheld when fewer than 5 stores outside the caller's own stand behind it. No store in the group is ever named. - `compare_to_name` (string | null): The district, state/region or category to compare against. Omit it for a category or state to use the caller's own (e.g. "stores in my format"); a district must be named, and leaving it out returns the caller's own districts to choose from. ### `get-product-mix` Sales and quantity split by product CATEGORY for a single location or network-wide over a date range, ranked by net sales. Built from the retained `sales_by_category` digest metadata. Each category row carries its stable `category_uuid` (null for uncategorized or deleted-category sales) and its `group` — the tenant-assigned business line it rolls up into (null when ungrouped). Pass "category" to focus the report on one line: it resolves group-first (a tenant group such as "Meal Plans") and falls back to a single category of that name, and the response echoes the match under `filter` with that line's net, quantity, and share of the whole mix. For individual menu items use the item-level tools: get-top-items (best sellers), get-item-performance (one item over time and across stores), compare-item-mix (what grew, declined, appeared or disappeared) and get-basket-metrics (checks, items and guests per check). The returned `total_net_sales` is the sum of category item sales (the basis for each category's share) and can differ from the `net_sales_amount` metric, which is order-level and nets in refunds, comps, and other adjustments not attributed to a category. A network mix also covers open locations only, while a network `net_sales_amount` total also counts closed (deactivated) locations for the days they traded, so over a range in which a location closed the gap includes that location's sales. Returns empty if the tenant has not populated category metadata. Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. - `category` (string | null): Optional category or group name to focus on. Resolved group-first (a tenant business line such as "Meal Plans"), then falling back to a single category of that name. - `top` (integer | null): Limit to the top N categories by net sales (default: all) ### `get-ratings-report` Average customer rating per source (Google, Yelp, …) for a single location or network-wide over a date range, in one call — enumerating exactly the rating sources this tenant has enabled. Google and Yelp publish only a lifetime rating — the all-time average of every review a location has ever received, re-read weekly — so their average here is the mean of the lifetime readings taken in the range, not a rating earned during it; those sources are marked is_lifetime_average with a note saying so. Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. For a per-source time series use get-metric-trendline with metric=rating and a source. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. ### `get-retention-report` Employee turnover bundle for a single location or network-wide over a date range, in one call: manager turnover rate and non-manager turnover rate (both ratios, e.g. 0.25 = 25%, averaged across retention reports in the range). Omit "location" for a network report; franchisee tokens are automatically scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide report. ### `get-risk-findings` Risk Assessment flags: stores whose discount rate or $0-check rate is outside their usual range — and, where the account scores it, whose cash share of payments has fallen well below its own norm for weeks (a low cash share can mean cash sales not rung up; it can also mean a new kiosk or payment habit) — and why. A "flag" is a risk finding. Each finding carries factors — plain sentences stating the evidence (the store against its own history, against comparable stores, trend, the other rate, network moves, check volume); explain a flag by quoting them rather than inferring a cause, and never present a flag as evidence of wrongdoing. By default returns unresolved findings (open or acknowledged), worst tier first and longest-running first within a tier; weeks_open counts the opening week as week 1, and total is the count before the limit. summary carries precomputed counts over every matching finding, before the limit: findings and distinct stores overall, per metric (by_metric, each with by_tier) and per tier (by_tier), and stores_on_both_metrics (stores flagged on more than one rate) with their names. Quote these for any count, heading, or "flagged on both" list — never count or group the rows yourself, and say when findings were cut by the limit. Pass location for one store (the response then adds latest_assessment for each rate the account scores). Rates are percents. Franchisee tokens see only their own stores' findings, without tier names, and never network findings; include_network (franchisor only) adds network_findings, where much of the network moved together. Requires the Risk Assessment feature. - `location` (string | null): Location identifier (UUID, external_id, or name) — omit for every store in scope - `metric` (string | null): Only this rate: discount_rate, zero_net_check_rate, or cash_share (only where the account scores cash share) - `status` (string | null): open, acknowledged, explained, cleared, or any. Default: open and acknowledged (still unresolved) - `min_tier` (string | null): Franchisor only: watch, elevated, or high — findings at or above this tier - `opened_since` (string | null): Only findings opened on or after this date (YYYY-MM-DD) - `include_network` (boolean | null): Franchisor only: also return network-wide shift findings - `limit` (integer | null): Max findings to return (default 25, max 100) ### `get-score-drivers` Explain WHY a single location's SEEScore is what it is — the per-metric breakdown that produced it. Returns each weighted component metric with its impact weight, raw 0–10 performance multiplier and tier (poor/adequate/excellent), the scoring curve used (linear/step), the graded value against its target band (low_lt/high_gt, when available), earned vs. max points, and point gap (improvement opportunity), plus the location's last 4 weeks of weekly score trend. Use this when the composite score (from get-leaderboard / get-metric-snapshot / get-metric-trendline) isn't enough and you need to know which metrics are dragging the score down or holding it up. Single location only. Identify the location by UUID, external_id, or name. - `location` (string, required): Location identifier (UUID, external_id, or name) ### `get-tenant-context` Get tenant configuration including district group names, category group names, and feature settings. Call this first to learn the tenant vocabulary. ### `get-tender-mix` How guests paid (#882): payments by tender over a date range, from check-level POS data — the same figures as the Tender card on Sales Analytics. `tenders` lists each tender (its POS name, e.g. "Visa", "Cash", "DoorDash") with payments, amount, share (of the payment amount; Brink refund lines are netted in, Qu refunds are not recorded yet), avg_payment and tip_amount. `types` groups them by the POS's own tender category ("Credit card", "External", "Cash" on Brink; "Credit", "Offline Credit", "Gift Card" on Qu — present them as the POS names them; Qu's "Offline Credit" mixes cards with delivery, catering and A/R, so `types_note` says so). `cash_share` is the scope's cash share of payments and `network_average_cash_share` the average store's across the network (stores with under 30 payments left out; anonymised, so franchisee tokens get it too, null for them when fewer than five other stores stand behind it). `card_brands` splits card payments by brand where the POS records one (Qu); on Brink the card tenders are the brands themselves, so it is empty. Shares are of payments taken, which run a little under check totals: never call them a share of sales. Pass `by_location: true` for each store's total and split by type (the stores in scope only). Franchisee tokens are scoped to their authorized locations and never receive other stores. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for network-wide. - `by_location` (boolean | null): Include each store's total and split by tender type (default false) - `top` (integer | null): How many tenders to list (default 25, max 50) ### `get-top-items` Best-selling menu ITEMS for a single location or network-wide over a date range, from check-level POS data. Rank by net sales (default), quantity, checks (how many checks included the item), or voids (voided units — what gets taken off checks most). Each row carries the POS `item_external_id` (pass it to get-item-performance), the item name, its category and group, net_sales, quantity, checks, check_penetration (share of all checks in scope that included the item), avg_unit_price (net per unit), share of the item total, and void_quantity / void_amount (voided units and their value; voided lines are never in the sales figures; a 0 void_amount beside voided units means the POS sent no price on the voided lines, so report the units, not "$0 voided"). Pass "category" to rank within one business line: it resolves group-first (e.g. "Meal Plans") and falls back to a single category. `top` defaults to 15 (max 50). The list holds only the top items by rank_by, so a superlative holds only for that measure: rows ranked by net_sales do not show which item sold the most units or reached the most checks — call again with rank_by quantity, checks or voids before claiming that, and otherwise say "among these top items". `groups` splits the listed items by group (category when ungrouped) with the item count, net sales and share of each — quote it for "how many of these are X" rather than counting rows. Top-level lines only unless include_modifiers is true (then its modifier sales add in too, and its checks are the larger of the two counts, since one check can hold both); the parts and add-ons of a Qu combo are modifier lines. Pass "daypart" (a name from get-daypart-mix, e.g. Breakfast) to rank within one daypart: an item counts toward the daypart its check was opened in, and check_penetration is then the share of that daypart's checks; accounts without dayparts get an error. Service charges and lines that never carried revenue (order notes, loyalty markers) are excluded; an item that was only voided is kept. `total_net_sales` is the item total in scope — it can differ from the order-level net_sales_amount metric. A `data_from` note means the range starts before item data exists; a `message` means item data is not available for this account. Franchisee tokens are scoped to their authorized locations. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `location` (string | null): Optional location identifier (UUID, external_id, or name). Omit for a network-wide ranking. - `category` (string | null): Optional category or group name to rank within. Resolved group-first, then a single category. - `rank_by` (string | null): net_sales (default), quantity, checks, or voids - `top` (integer | null): How many items to return (default 15, max 50) - `include_modifiers` (boolean | null): Include modifiers and add-on lines as items (default false) - `daypart` (string | null): Optional daypart (its name or key, e.g. "Breakfast", from get-daypart-mix) to read within: figures count only checks opened in that daypart, and check penetration is over that daypart's checks. Only for accounts with dayparts configured. ### `list-available-metrics` List all metrics enabled for this tenant, including display names, types (currency/percentage), and behavior (lower-is-better). Use the metric key values when calling leaderboard tools. ### `report-tool-issue` Record that these tools could not answer a request, so the FranchiseSEE team can build the missing reports. It applies when no tool covers the request, when a call succeeded but did not have the intended effect (for example it returned nothing where data should exist), when a tool kept failing, or when no accepted argument could be worked out. Describe the gap in terms of reports, metrics, stores and dates. Do not quote the user, and leave out names, contact details and other personal information. It changes no store data. - `category` (string, required): What kind of problem: capability_gap (no tool exists for what the user asked), unexpected_result (a call succeeded but did not have the effect you intended), tool_failure (a tool errored or kept erroring), invalid_argument (you could not work out an argument the tool would accept), other - `tool` (string | null): The exact name of the tool involved, if there is one — for example "update-location". Leave empty for a capability gap: that is the case where no tool exists. - `goal` (string, required): The report, metric, stores and period that were needed — for example "labor percentage by daypart for one location, last month". Do not quote the user or include names or other personal details. - `details` (string, required): What happened and what was needed instead. If arguments did not work, say which. No personal details. ## Admin (`franchisesee-admin`) Server URL: `https://app.franchisesee.com/mcp/admin` ### `add-location-groups-for-user` Add location groups to a user, appending to their existing groups (unlike sync-location-groups-for-user, which replaces all groups). A user's access is either a single direct location OR location groups — so assigning groups clears any direct location assignment. Requires MANAGE_USERS permission. - `identifier` (string, required): User identifier (email or ID) - `location_group_identifiers` (array, required): Array of location group identifiers (UUID, external_id, or name) to add ### `bulk-add-location-groups-for-users` Add location groups to multiple users in a single operation. Each entry specifies a user (by email) and location groups to add. Groups are appended to existing assignments — existing group memberships are preserved. A user given groups loses their direct location: access is one direct location or groups, never both. Ideal for processing CSV-style bulk user-to-group assignments. - `assignments` (array, required): Array of {email, location_groups: [identifier, ...]} pairs. Each location_group can be a UUID, external_id, or name. ### `bulk-assign-locations-to-district` Assign multiple locations to a district in a single operation. Each entry specifies a location and a district. The district_group_name determines which district slot to update (e.g., "Region" → district_id, "Market" → district_id_2). Use get-tenant-context to see the tenant's district group names. Ideal for processing CSV-style bulk reassignments. - `district_group_name` (string, required): District group name (e.g., "Region", "Market") — determines which district slot to update - `assignments` (array, required): Array of {location, district} pairs. Each location/district can be a UUID, external_id, or name. ### `create-district` Create a new district within a district group. Specify the group by its tenant-customized name. Requires MANAGE_LOCATIONS permission. - `name` (string, required): District name (required) - `district_group_name` (string, required): District group name (required) — use get-tenant-context to see available groups - `external_id` (string | null): External identifier ### `create-location` Create a new location. Requires MANAGE_LOCATIONS permission. - `name` (string, required): Location name (required) - `address` (string | null): Street address - `city` (string | null): City - `region` (string | null): State/region - `postal_code` (string | null): Postal code - `country` (string | null): Country - `external_id` (string | null): External identifier (must be unique within tenant) - `district` (string | null): District identifier (UUID, external_id, or name) - `district2` (string | null): Secondary district identifier (UUID, external_id, or name) - `location_category` (string | null): Location category identifier (UUID, external_id, or name) - `opened_on` (string | null): The first day the location traded, as YYYY-MM-DD. Same-store (comp) comparisons only count a location that was open for the whole comparison window; omit when unknown. - `is_active` (boolean | null): Activate the location immediately. Omit — or send false — to leave it pending, in which case it collects no metrics and appears on no leaderboard until someone activates it. Where the tenant ranks locations within a category, an active location must be given one from the primary category group. ### `create-location-category` Create a new location category within a category group. Specify the group by its tenant-customized name. Requires MANAGE_LOCATIONS permission. - `name` (string, required): Category name (required) - `category_group_name` (string, required): Category group name (required) — use get-tenant-context to see available groups - `external_id` (string | null): External identifier - `sort_order` (integer | null): Sort order (0-based) ### `create-location-group` Create a new location group. Requires MANAGE_LOCATIONS permission. - `name` (string, required): Group name (required) - `external_id` (string | null): External identifier (must be unique within tenant) ### `delete-district` Delete a district. Cannot delete districts that have locations assigned. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): District identifier (UUID, external_id, or name) ### `delete-location` Permanently delete a location and all its history. Only a location whose is_deletable is true (see list-locations) can be deleted: a pending one, a deactivated one that never loaded any sales, labor or custom-metric data, or a deactivated one whose history has aged out of reporting retention. A closed store keeps counting toward network totals and year-over-year benchmarks until then (deletable_on gives the date), so the call is refused before it. To take a store off leaderboards, deactivate it with update-location instead. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): Location identifier (UUID, external_id, or name) ### `delete-location-category` Delete a location category. Cannot delete categories that have locations assigned. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): Category identifier (UUID, external_id, or name) ### `delete-location-group` Delete a location group. Cannot delete groups that have locations or users assigned. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): Location group identifier (UUID, external_id, or name) ### `delete-user` Delete a user. Identify by email or ID. Requires MANAGE_USERS permission. - `identifier` (string, required): User identifier (email or ID) ### `get-tenant-context` Get tenant configuration including district group names, category group names, and feature settings. Call this first to learn the tenant vocabulary. ### `list-available-metrics` List all metrics enabled for this tenant, including display names, types (currency/percentage), and behavior (lower-is-better). Use the metric key values when calling leaderboard tools. ### `list-districts` List districts, optionally filtered by district group name or external_id. Include districtGroup to see which group each district belongs to. - `district_group_name` (string | null): Filter by district group name (tenant-customized) - `filter_external_id` (string | null): Filter by external_id - `include` (string | null): Comma-separated includes: districtGroup - `per_page` (integer | null): Results per page (1–100; default 30) ### `list-location-categories` List location categories, optionally filtered by category group name or external_id. - `category_group_name` (string | null): Filter by category group name (tenant-customized) - `filter_external_id` (string | null): Filter by external_id - `include` (string | null): Comma-separated includes: locationCategoryGroup - `per_page` (integer | null): Results per page (1–100; default 30) ### `list-location-groups` List location groups with optional location includes and external_id filter. - `filter_external_id` (string | null): Filter by external_id - `include_locations` (boolean | null): Include the locations in each group - `per_page` (integer | null): Results per page (1–100; default 30) ### `list-locations` List locations with optional filters. Supports filtering by external_id and including related district, category, and group data. - `filter_external_id` (string | null): Filter by external_id - `include` (string | null): Comma-separated includes: locationCategory, district, district2, locationGroups - `per_page` (integer | null): Results per page (1–100; defaults to 100). Use page to fetch more. - `page` (integer | null): Page number - `sort` (string | null): Sort field: name, created_at, updated_at. Prefix with - for descending. ### `list-users` List users for the current tenant. Supports filtering by email and perspective (franchisor/franchisee). - `filter_email` (string | null): Filter by email address - `filter_perspective` (string | null): Filter by perspective: franchisor, franchisee - `include` (string | null): Comma-separated includes: locationGroups, location - `per_page` (integer | null): Results per page (1–100; default 30) - `sort` (string | null): Sort field: first_name, last_name, email, created_at, updated_at ### `report-tool-issue` Record that these tools could not carry out a request, so the FranchiseSEE team can build the missing capabilities. It applies when no tool covers the request, when a call succeeded but did not have the intended effect, when a tool kept failing, or when no accepted argument could be worked out. Describe the gap in terms of reports, metrics, stores and dates. Do not quote the user, and leave out names, contact details and other personal information. It changes no store data. - `category` (string, required): What kind of problem: capability_gap (no tool exists for what the user asked), unexpected_result (a call succeeded but did not have the effect you intended), tool_failure (a tool errored or kept erroring), invalid_argument (you could not work out an argument the tool would accept), other - `tool` (string | null): The exact name of the tool involved, if there is one — for example "update-location". Leave empty for a capability gap: that is the case where no tool exists. - `goal` (string, required): The report, metric, stores and period that were needed — for example "labor percentage by daypart for one location, last month". Do not quote the user or include names or other personal details. - `details` (string, required): What happened and what was needed instead. If arguments did not work, say which. No personal details. ### `sync-location-groups-for-location` Set which location groups a location belongs to. Replaces all current group assignments. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): Location identifier (UUID, external_id, or name) - `location_group_identifiers` (array, required): Array of location group identifiers (UUID, external_id, or name) ### `sync-location-groups-for-user` Set which location groups a user belongs to. Replaces all current group assignments and clears the user's direct location assignment. Pass an empty array to remove all of the user's group access (leaving them with no assigned locations). Requires MANAGE_USERS permission. - `identifier` (string, required): User identifier (email or ID) - `location_group_identifiers` (array, required): Array of location group identifiers (UUID, external_id, or name) ### `update-district` Update an existing district. Identify by UUID, external_id, or name. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): District identifier (UUID, external_id, or name) - `name` (string | null): New district name - `external_id` (string | null): External identifier ### `update-location` Update an existing location. Identify by UUID, external_id, or name. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): Location identifier (UUID, external_id, or name) - `name` (string | null): New location name - `address` (string | null): Street address - `city` (string | null): City - `region` (string | null): State/region - `postal_code` (string | null): Postal code - `country` (string | null): Country - `external_id` (string | null): External identifier - `district` (string | null): District identifier (UUID, external_id, or name). Send null to unassign the location from its district; omit to leave it unchanged. - `district2` (string | null): Secondary district identifier. Send null to unassign; omit to leave it unchanged. - `location_category` (string | null): Location category identifier (UUID, external_id, or name). Send null to unassign; omit to leave it unchanged. - `opened_on` (string | null): The first day the location traded, as YYYY-MM-DD. Same-store (comp) comparisons only count a location that was open for the whole comparison window; unknown (null) leaves it to the sales signal alone. Send null to clear; omit to leave it unchanged. - `is_active` (boolean | null): Activation state. Omit to leave it unchanged. False takes an active location out of service (off leaderboards, kept in historical network totals); true brings it back. False on a location that was never activated leaves it pending rather than deactivated. Where the tenant ranks locations within a category, an activated location must have one from the primary category group. ### `update-location-category` Update an existing location category. Identify by UUID, external_id, or name. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): Category identifier (UUID, external_id, or name) - `name` (string | null): New category name - `external_id` (string | null): External identifier - `sort_order` (integer | null): Sort order (0-based) ### `update-location-group` Update an existing location group. Identify by UUID, external_id, or name. Requires MANAGE_LOCATIONS permission. - `identifier` (string, required): Location group identifier (UUID, external_id, or name) - `name` (string | null): New group name - `external_id` (string | null): External identifier ### `update-user` Update an existing user. Identify by email or ID. Requires MANAGE_USERS permission. - `identifier` (string, required): User identifier (email or ID) - `first_name` (string | null): First name - `last_name` (string | null): Last name - `perspective` (string | null): User perspective: franchisor or franchisee. Can only be set while the user has none (during provisioning) — a set perspective is immutable. To change what kind of user someone is, delete the user and re-invite them. - `location` (string | null): Assigned location identifier (UUID, external_id, or name) — for franchisee users. Setting a location removes all of the user's location-group assignments (a user has a direct location or groups, never both). - `roles` (array | null): Array of role names to assign --- # Get Authenticated User `GET /api/user` Returns the account the token acts as, serialized as a `User` resource. Available to any authenticated token regardless of its abilities. Internal columns (such as the integer `location_id` foreign key) are never exposed; `location_id` is surfaced as the location's UUID. ## Example request ```bash curl "https://app.franchisesee.com/api/user" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 A single User. ```json { "data": { "id": "6e2b9d41-8c7a-4f3e-b1d5-9a0c2e4f6b81", "first_name": "Jordan", "last_name": "Rivera", "email": "jordan.rivera@example.com", "perspective": "admin", "location_id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "has_sso_auth": true, "roles": [ "string" ], "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "is_private": false, "is_corporate": false, "is_active": true, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": {}, "district2": {}, "locationCategory": {}, "locationGroups": [] } ] } ], "location": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "is_private": false, "is_corporate": false, "is_active": true, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ {} ] } ] } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` --- # Get Metrics `GET /api/v1/data/metrics` Retrieves the metric reference-data options available to the tenant. Requires the `read` ability. ## Example request ```bash curl "https://app.franchisesee.com/api/v1/data/metrics" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "data": [ { "value": "net_sales_amount", "label": "Net sales" } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Get Sources `GET /api/v1/data/sources` Retrieves the source reference-data options. When `metric` is supplied, the result is narrowed to the sources valid for that metric. Requires the `read` ability. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `metric` | string | no | Optional metric key to scope the returned sources. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/data/sources" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "data": [ { "value": "net_sales_amount", "label": "Net sales" } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Get Periods `GET /api/v1/data/periods` Retrieves the period reference-data options (excludes `custom`). Requires the `read` ability. ## Example request ```bash curl "https://app.franchisesee.com/api/v1/data/periods" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "data": [ { "value": "net_sales_amount", "label": "Net sales" } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Get Leaderboard `GET /api/v1/data/leaderboard` Retrieves a ranked leaderboard of locations for a single metric over a period. Franchisor-scoped tokens only (franchisee tokens receive 403). Requires the `read` ability. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `period` | string | yes | | | `metric` | string | yes | Metric key, e.g. `net_sales_amount`, `custom:loyalty_signups`, or `competition:spring_challenge`. Competition metrics rank locations by their current cumulative standing in the latest competition for that metric; the requested period does not change the competition window. | | `source` | string | no | | | `district_id` | string | no | | | `district_id_2` | string | no | | | `location_category_id` | string | no | | | `state` | string | no | | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/data/leaderboard?period=current&metric=string" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "data": { "period": "last_week", "metric": "net_sales_amount", "source": null, "starts_on": "2026-11-02", "ends_on": "2026-11-08", "num_locations": 48, "closed_location_count": 0, "total": 1268430.55, "median": 25890.12, "mean": 26425.64, "benchmark_stats": { "prior": { "total": 1221875.1, "median": 25102.44, "mean": 25455.73, "location_count": 48, "closed_location_count": 0 }, "yoy": { "total": 1221875.1, "median": 25102.44, "mean": 25455.73, "location_count": 48, "closed_location_count": 0 } }, "locations": [ { "location_id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "name": "Downtown Austin", "value": 41285.9, "rank": 1, "benchmarks": { "prior": { "previous_value": 39870.25, "previous_rank": 2, "previous_on": "Oct 26 – Nov 1, 2026", "rank_change": -1, "value_change": 1415.65, "value_change_rate": 0.0355 }, "yoy": { "previous_value": 39870.25, "previous_rank": 2, "previous_on": "Oct 26 – Nov 1, 2026", "rank_change": -1, "value_change": 1415.65, "value_change_rate": 0.0355 } } } ] } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Query Metric Value `GET /api/v1/data/metrics/query` Retrieves a single aggregated value (summed or averaged) for a metric over a date range. With `location_id`, the caller must have access to that location; without it, the value aggregates across all tenant locations and the token must be franchisor-scoped. Requires the `read` ability. Competition metrics (`competition:*`) are point-in-time standings rather than range aggregates: the value is the cumulative standing as of `end_on` (clamped to the competition's end date), and `aggregation_method` is `point_in_time`. A network query rolls standings up per the metric's configured summability — summed for common-unit values (e.g. dollars raised), averaged for per-location rates. The competition resolved is the latest started, rankable competition for the metric whose window overlaps the requested range; `value` is null when no such competition overlaps the range or it has no data yet, and the response names the resolved competition and its window in a `competition` block so the figure stays attributable. Note this differs from Get Leaderboard, which always ranks the latest competition for the metric regardless of the requested period — for a historical range the two endpoints can resolve different competitions. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `metric` | string | yes | Metric key, e.g. `net_sales_amount`, `custom:loyalty_signups`, or `competition:spring_challenge`. | | `source` | string | no | | | `start_on` | string | yes | Start of the date range (inclusive), formatted `YYYY-MM-DD`. | | `end_on` | string | yes | End of the date range (inclusive), formatted `YYYY-MM-DD`. Must be on or after start_on. | | `location_id` | string | no | Location UUID to scope the value to a single location. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/data/metrics/query?metric=string&start_on=2026-11-12&end_on=2026-11-12" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "data": { "value": 184250.37, "aggregation_method": "sum", "competition": { "id": "d4f6a8c0-2e5b-4c7d-9f1a-6b3e8d0c2a54", "name": "Summer Smoothie Challenge", "starts_on": "2026-06-01", "ends_on": "2026-08-31" } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The selected metric is invalid.", "errors": { "metric": [ "The selected metric is invalid." ] } } ``` --- # Query Metric Trendline `GET /api/v1/data/metrics/trendline` Retrieves a day-by-day series of aggregated values for a metric over a date range. Same access rules as Query Metric Value. Requires the `read` ability. For competition metrics (`competition:*`) the series is a running-total curve, not per-day amounts: each point rolls up the latest known standing, as of that day, of every in-scope location that has one. The series is truncated to the competition's window regardless of the requested range, and carries one point per bucket end — every day by default, the last digested day of each week or month when grouped (grouping is available on the MCP tool, not this endpoint). When the range resolves a standing (the same condition under which Query Metric Value returns a value), the response names the resolved competition and its window in a top-level `competition` field, alongside `data`. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `metric` | string | yes | Metric key, e.g. `net_sales_amount`, `custom:loyalty_signups`, or `competition:spring_challenge`. | | `source` | string | no | | | `start_on` | string | yes | Start of the date range (inclusive), formatted `YYYY-MM-DD`. | | `end_on` | string | yes | End of the date range (inclusive), formatted `YYYY-MM-DD`. Must be on or after start_on. | | `location_id` | string | no | Location UUID to scope the series to a single location. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/data/metrics/trendline?metric=string&start_on=2026-11-12&end_on=2026-11-12" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "data": [ { "date": "2026-06-01", "value": 6142.18 } ], "competition": { "id": "d4f6a8c0-2e5b-4c7d-9f1a-6b3e8d0c2a54", "name": "Summer Smoothie Challenge", "starts_on": "2026-06-01", "ends_on": "2026-08-31" } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The selected metric is invalid.", "errors": { "metric": [ "The selected metric is invalid." ] } } ``` --- # List Risk Findings `GET /api/v1/risk/findings` Risk Assessment flags: stores whose discount rate or $0-check rate is outside their usual range, each with the plain-sentence factors the Risk drill-down shows. By default returns unresolved findings (open or acknowledged), worst tier first and, within a tier, longest-running first. Requires the `read` ability, the Risk Assessment feature, and the `view risk assessment` permission; a franchisee token additionally needs risk to be visible to franchisees. Franchisee tokens see only their own stores' findings, receive no `tier`, `peak_tier`, or `escalation` (every flag reads "Outside your usual range" in `tier_label`), and never receive network findings. The same report backs the `get-risk-findings` MCP tool. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `location` | string | no | Location UUID. Adds `latest_assessment` for each rate the account scores to the response. A franchisee token asking for a location outside its portfolio receives 403. | | `metric` | string | no | One rate. `cash_share` is scored only where the account has cash share turned on; it is flagged when the store's cash share falls well below its own norm. | | `status` | string | no | One status, or `any`. Default open and acknowledged. | | `min_tier` | string | no | Findings at or above this tier. Ignored for franchisee tokens. | | `opened_since` | string | no | Only findings opened on or after this date, formatted `YYYY-MM-DD`. | | `include_network` | boolean | no | Franchisor tokens only: also return `network_findings`, where much of the network moved together. Ignored for franchisee tokens. | | `limit` | integer | no | Maximum findings to return (1–100, default 25). | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/risk/findings" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "data": { "latest_assessed_on": "2026-11-11", "count": 1, "total": 3, "summary": { "findings": 3, "stores": 2, "by_metric": { "field": { "label": "Discount rate", "findings": 2, "stores": 2, "by_tier": { "field": 1 } } }, "by_tier": { "field": 1 }, "stores_on_both_metrics": { "count": 1, "stores": [ "string" ] } }, "findings": [ { "id": "0f7d3b9e-5a1c-4e8f-b2d6-7c4a9e1f3b85", "location": { "uuid": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "name": "Downtown Austin", "district": "Central Texas", "category": "Inline" }, "metric": "discount_rate", "metric_label": "Discount rate", "kind": "spike", "kind_label": "Spike", "tier": "elevated", "tier_label": "Elevated", "peak_tier": "elevated", "status": "explained", "status_label": "Explained", "opened_on": "2026-10-27", "last_seen_on": "2026-11-10", "closed_on": null, "weeks_open": 3, "escalation": { "on": "2026-11-03", "from": "watch", "to": "elevated" }, "acknowledged_by": null, "explanation": { "reason": "approved_promotion", "reason_label": "Approved promotion", "note": "BOGO bowls weekend run by the local marketing co-op.", "by": "Jordan Rivera", "on": "2026-11-04", "promotion_window": { "name": "BOGO Bowls Weekend", "starts_on": "2026-11-07", "ends_on": "2026-11-09" } }, "factors": [ { "key": "own_history", "status": "hit", "text": "Discount rate was 6.8% last week, against a usual 2.1% for this store." } ] } ], "network_findings": [ { "id": "2c8e4a6f-9b1d-4f3a-8e5c-1d7b3f9a5e26", "location": {}, "metric": "zero_net_check_rate", "metric_label": "$0-check rate", "status": "open", "status_label": "Open", "opened_on": "2026-10-20", "last_seen_on": "2026-11-10", "closed_on": null, "window_starts_on": "2026-10-13", "breadth_pct": 41.7, "movers": 20, "eligible": 48, "network_rate_pct": 1.9, "explanation": {} } ], "latest_assessment": { "field": { "assessed_on": "2026-11-10", "rate_pct": 6.8, "usual_rate_pct": 2.1, "comparable_stores_rate_pct": 2.4, "comparable_store_count": 12, "checks": 1834, "flagged": true, "tier_label": "Elevated", "suppression": null, "suppression_label": null } } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The selected metric is invalid.", "errors": { "metric": [ "The selected metric is invalid." ] } } ``` --- # Get Locations `GET /api/v1/locations` Retrieves a paginated list of Location objects. Requires the `read` ability. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page[number]` | integer | no | Page number (starts at 1). | | `page[size]` | integer | no | Results per page (default 30, max 100). | | `filter[external_id]` | string | no | | | `sort` | string | no | Allowed: name, created_at, updated_at (prefix with `-` for descending). Default: name. | | `include` | string | no | Comma-separated. Allowed: locationCategory, district, district2, locationGroups. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/locations" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "links": { "first": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "last": "https://app.franchisesee.com/api/v1/locations?page[number]=10", "prev": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "next": "https://app.franchisesee.com/api/v1/locations?page[number]=3" }, "meta": { "current_page": 2, "from": 26, "last_page": 10, "path": "https://app.franchisesee.com/api/v1/locations", "per_page": 25, "to": 50, "total": 250 }, "data": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ {} ] } ] } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Create Location `POST /api/v1/locations` Creates a new Location object. Requires the `create` ability. The location is pending (inactive) unless `is_active: true` is sent. ## Request body ```json { "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "is_active": true, "external_id": "STR-1042", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "brink_api_key": "brk_live_8f2c4a1e9d7b" } } } ``` ## Example request ```bash curl -X POST "https://app.franchisesee.com/api/v1/locations" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "is_active": true, "external_id": "STR-1042", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "brink_api_key": "brk_live_8f2c4a1e9d7b" } } }' ``` ## Responses ### 201 A single Location. ```json { "data": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": {}, "district2": {}, "locationCategory": {}, "locationGroups": [] } ] } ] } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Get Location `GET /api/v1/locations/{id}` Retrieves a single Location object. Requires the `read` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `include` | string | no | Comma-separated. Allowed: locationCategory, district, district2, locationGroups. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/locations/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 A single Location. ```json { "data": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": {}, "district2": {}, "locationCategory": {}, "locationGroups": [] } ] } ] } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Patch Location `PATCH /api/v1/locations/{id}` Partially updates a Location object (send only the fields to change). Requires the `update` ability. Send `is_active` to activate or deactivate the location. Deactivating (`is_active: false`) is how a store closes: it leaves leaderboards, but its history keeps counting toward network totals and year-over-year benchmarks, so it does not by itself make the location deletable — see Delete Location and `deletable_on`. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Request body ```json { "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "is_active": true, "external_id": "STR-1042", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "brink_api_key": "brk_live_8f2c4a1e9d7b" } } } ``` ## Example request ```bash curl -X PATCH "https://app.franchisesee.com/api/v1/locations/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "is_active": true, "external_id": "STR-1042", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "brink_api_key": "brk_live_8f2c4a1e9d7b" } } }' ``` ## Responses ### 200 A single Location. ```json { "data": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": {}, "district2": {}, "locationCategory": {}, "locationGroups": [] } ] } ] } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Delete Location `DELETE /api/v1/locations/{id}` Permanently deletes a Location and everything it holds: daily history, orders, shifts, scores, ratings, store audits, COGS and retention report records, competition standings, and any users assigned directly to it. Requires the `delete` ability. Only a location whose `is_deletable` is true can be deleted: a pending location (never activated), a deactivated one that never loaded any sales, labor or custom-metric data (keyed in by mistake), or a deactivated one whose daily history has aged out of reporting retention. A closed store's sales count toward network totals and year-over-year benchmarks for two full calendar years after the year it closed, so a deactivated location becomes deletable on Jan 1 three years after its deactivation year (four for a Dec 31 deactivation, as that day is itself retained); `deletable_on` gives the date. Deactivating (`is_active: false`) is what takes a store off leaderboards; deletion is cleanup only. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Example request ```bash curl -X DELETE "https://app.franchisesee.com/api/v1/locations/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 204 Deleted. ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 409 The location cannot be deleted yet: it is active, or it is deactivated and still inside reporting retention. The message names the date it becomes deletable. ```json { "message": "Cannot delete an active location. Deactivate it first." } ``` --- # Bulk Assign Locations to Districts `POST /api/v1/locations/bulk-district-assignments` Assigns many locations to districts within a single district group in one request. The group's `index` determines which slot is written (index 0 → `district_id`, otherwise `district_id_2`). Every assigned district must belong to the named district group. Requires the `update` ability and `create` permission on locations. ## Request body ```json { "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "assignments": [ { "location": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9" } ] } ``` ## Example request ```bash curl -X POST "https://app.franchisesee.com/api/v1/locations/bulk-district-assignments" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "assignments": [ { "location": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9" } ] }' ``` ## Responses ### 200 Success ```json { "district_group": "Region", "district_column": "district_id", "total": 3, "success_count": 2, "error_count": 1, "results": [ { "index": 0, "location": "Downtown Austin", "district": "Central Texas", "status": "success" } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Sync Location Groups `PUT /api/v1/locations/{id}/location-groups` Replaces the set of location groups a Location belongs to. `location_group_ids` is required (an empty array is rejected). Requires the `update` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Request body ```json { "location_group_ids": [ "019be24b-8fb3-7065-bcda-b3a26afdda1f", "019be24b-8fb2-718a-a686-cd8eb8834625" ] } ``` ## Example request ```bash curl -X PUT "https://app.franchisesee.com/api/v1/locations/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63/location-groups" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "location_group_ids": [ "019be24b-8fb3-7065-bcda-b3a26afdda1f", "019be24b-8fb2-718a-a686-cd8eb8834625" ] }' ``` ## Responses ### 200 The updated Location, with its `locationGroups` relationship loaded. ```json { "data": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": {}, "district2": {}, "locationCategory": {}, "locationGroups": [] } ] } ] } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Get Location Groups `GET /api/v1/location-groups` Retrieves a paginated list of LocationGroup objects. Requires the `read` ability. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page[number]` | integer | no | Page number (starts at 1). | | `page[size]` | integer | no | Results per page (default 30, max 100). | | `filter[external_id]` | string | no | | | `sort` | string | no | Allowed: name, created_at, updated_at. Default: name. | | `include` | string | no | Allowed: locations. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/location-groups" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "links": { "first": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "last": "https://app.franchisesee.com/api/v1/locations?page[number]=10", "prev": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "next": "https://app.franchisesee.com/api/v1/locations?page[number]=3" }, "meta": { "current_page": 2, "from": 26, "last_page": 10, "path": "https://app.franchisesee.com/api/v1/locations", "per_page": 25, "to": 50, "total": 250 }, "data": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": {} }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": {} }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": {} }, "locationGroups": [ {} ] } ] } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Create Location Group `POST /api/v1/location-groups` Creates a new LocationGroup object. Requires the `create` ability. ## Request body ```json { "name": "Austin Metro", "external_id": "AUS-METRO" } ``` ## Example request ```bash curl -X POST "https://app.franchisesee.com/api/v1/location-groups" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Austin Metro", "external_id": "AUS-METRO" }' ``` ## Responses ### 201 A single LocationGroup. ```json { "data": { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [] } ] } ] } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Get Location Group `GET /api/v1/location-groups/{id}` Retrieves a single LocationGroup object. Requires the `read` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `include` | string | no | Allowed: locations. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/location-groups/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 A single LocationGroup. ```json { "data": { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [] } ] } ] } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Patch Location Group `PATCH /api/v1/location-groups/{id}` Partially updates a LocationGroup object. Requires the `update` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Request body ```json { "name": "Austin Metro", "external_id": "AUS-METRO" } ``` ## Example request ```bash curl -X PATCH "https://app.franchisesee.com/api/v1/location-groups/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Austin Metro", "external_id": "AUS-METRO" }' ``` ## Responses ### 200 A single LocationGroup. ```json { "data": { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [] } ] } ] } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Delete Location Group `DELETE /api/v1/location-groups/{id}` Deletes a LocationGroup object that is not currently in use. Requires the `delete` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Example request ```bash curl -X DELETE "https://app.franchisesee.com/api/v1/location-groups/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 204 Deleted. ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Get Districts `GET /api/v1/districts` Retrieves a paginated list of District objects. Requires the `read` ability. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page[number]` | integer | no | Page number (starts at 1). | | `page[size]` | integer | no | Results per page (default 30, max 100). | | `filter[external_id]` | string | no | | | `filter[district_group_id]` | string | no | | | `sort` | string | no | Allowed: name, created_at, updated_at. Default: name. | | `include` | string | no | Allowed: districtGroup. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/districts" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "links": { "first": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "last": "https://app.franchisesee.com/api/v1/locations?page[number]=10", "prev": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "next": "https://app.franchisesee.com/api/v1/locations?page[number]=3" }, "meta": { "current_page": 2, "from": 26, "last_page": 10, "path": "https://app.franchisesee.com/api/v1/locations", "per_page": 25, "to": 50, "total": 250 }, "data": [ { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Create District `POST /api/v1/districts` Creates a new District object. Requires the `create` ability. ## Request body ```json { "name": "Central Texas", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "external_id": "CTX" } ``` ## Example request ```bash curl -X POST "https://app.franchisesee.com/api/v1/districts" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Central Texas", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "external_id": "CTX" }' ``` ## Responses ### 201 A single District. ```json { "data": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Get District `GET /api/v1/districts/{id}` Retrieves a single District object. Requires the `read` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `include` | string | no | Allowed: districtGroup. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/districts/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 A single District. ```json { "data": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Patch District `PATCH /api/v1/districts/{id}` Partially updates a District object. Requires the `update` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Request body ```json { "name": "Central Texas", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "external_id": "CTX" } ``` ## Example request ```bash curl -X PATCH "https://app.franchisesee.com/api/v1/districts/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Central Texas", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "external_id": "CTX" }' ``` ## Responses ### 200 A single District. ```json { "data": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Delete District `DELETE /api/v1/districts/{id}` Deletes a District object that is not currently in use. Requires the `delete` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Example request ```bash curl -X DELETE "https://app.franchisesee.com/api/v1/districts/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 204 Deleted. ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Get Location Categories `GET /api/v1/location-categories` Retrieves a paginated list of LocationCategory objects. Requires the `read` ability. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page[number]` | integer | no | Page number (starts at 1). | | `page[size]` | integer | no | Results per page (default 30, max 100). | | `filter[external_id]` | string | no | | | `filter[location_category_group_id]` | string | no | | | `sort` | string | no | Allowed: name, sort_order, created_at, updated_at. Default: sort_order. | | `include` | string | no | Allowed: locationCategoryGroup. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/location-categories" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "links": { "first": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "last": "https://app.franchisesee.com/api/v1/locations?page[number]=10", "prev": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "next": "https://app.franchisesee.com/api/v1/locations?page[number]=3" }, "meta": { "current_page": 2, "from": 26, "last_page": 10, "path": "https://app.franchisesee.com/api/v1/locations", "per_page": 25, "to": 50, "total": 250 }, "data": [ { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Create Location Category `POST /api/v1/location-categories` Creates a new LocationCategory object. Requires the `create` ability. ## Request body ```json { "name": "Inline", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "external_id": "INLINE", "sort_order": 1 } ``` ## Example request ```bash curl -X POST "https://app.franchisesee.com/api/v1/location-categories" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Inline", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "external_id": "INLINE", "sort_order": 1 }' ``` ## Responses ### 201 A single LocationCategory. ```json { "data": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Get Location Category `GET /api/v1/location-categories/{id}` Retrieves a single LocationCategory object. Requires the `read` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `include` | string | no | Allowed: locationCategoryGroup. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/location-categories/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 A single LocationCategory. ```json { "data": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Patch Location Category `PATCH /api/v1/location-categories/{id}` Partially updates a LocationCategory object. Requires the `update` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Request body ```json { "name": "Inline", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "external_id": "INLINE", "sort_order": 1 } ``` ## Example request ```bash curl -X PATCH "https://app.franchisesee.com/api/v1/location-categories/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "name": "Inline", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "external_id": "INLINE", "sort_order": 1 }' ``` ## Responses ### 200 A single LocationCategory. ```json { "data": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Delete Location Category `DELETE /api/v1/location-categories/{id}` Deletes a LocationCategory object that is not currently in use. Requires the `delete` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Example request ```bash curl -X DELETE "https://app.franchisesee.com/api/v1/location-categories/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 204 Deleted. ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Get Users `GET /api/v1/users` Retrieves a paginated list of User objects. Requires the `read` ability. HQ tokens list every user in the brand; a franchisee token lists the users at its own location, or only itself when it reaches stores through location groups. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page[number]` | integer | no | Page number (starts at 1). | | `page[size]` | integer | no | Results per page (default 30, max 100). | | `filter[email]` | string | no | | | `filter[perspective]` | string | no | | | `sort` | string | no | Allowed: first_name, last_name, email, created_at, updated_at. Default: email. | | `include` | string | no | Allowed: locationGroups, location. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/users" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "links": { "first": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "last": "https://app.franchisesee.com/api/v1/locations?page[number]=10", "prev": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "next": "https://app.franchisesee.com/api/v1/locations?page[number]=3" }, "meta": { "current_page": 2, "from": 26, "last_page": 10, "path": "https://app.franchisesee.com/api/v1/locations", "per_page": 25, "to": 50, "total": 250 }, "data": [ { "id": "6e2b9d41-8c7a-4f3e-b1d5-9a0c2e4f6b81", "first_name": "Jordan", "last_name": "Rivera", "email": "jordan.rivera@example.com", "perspective": "admin", "location_id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "has_sso_auth": true, "roles": [ "string" ], "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ {} ] } ], "location": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [] } ] } } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` --- # Bulk Add Location Groups to Users `POST /api/v1/users/bulk-location-groups` Appends location groups to many users in one request (append semantics — existing groups are preserved). Requires the `update` ability and the MANAGE_USERS permission. ## Request body ```json { "assignments": [ { "user": "6e2b9d41-8c7a-4f3e-b1d5-9a0c2e4f6b81", "location_group_ids": [ "9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" ] } ] } ``` ## Example request ```bash curl -X POST "https://app.franchisesee.com/api/v1/users/bulk-location-groups" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "assignments": [ { "user": "6e2b9d41-8c7a-4f3e-b1d5-9a0c2e4f6b81", "location_group_ids": [ "9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" ] } ] }' ``` ## Responses ### 200 Success ```json { "total": 2, "success_count": 2, "error_count": 0, "results": [ { "index": 0, "user": "jordan.rivera@example.com", "status": "success", "groups_added": 1, "total_groups": 2 } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Get User `GET /api/v1/users/{id}` Retrieves a single User object. Requires the `read` ability. A franchisee token may read only the users it could list; any other user returns 403. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `include` | string | no | Allowed: locationGroups, location. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/users/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 A single User. ```json { "data": { "id": "6e2b9d41-8c7a-4f3e-b1d5-9a0c2e4f6b81", "first_name": "Jordan", "last_name": "Rivera", "email": "jordan.rivera@example.com", "perspective": "admin", "location_id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "has_sso_auth": true, "roles": [ "string" ], "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": {}, "district2": {}, "locationCategory": {}, "locationGroups": [] } ] } ], "location": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ {} ] } ] } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Patch User `PATCH /api/v1/users/{id}` Partially updates a User object. Requires the `update` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Request body ```json { "first_name": "Jordan", "last_name": "Rivera", "roles": [ "string" ], "perspective": "admin", "location_id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e" } ``` ## Example request ```bash curl -X PATCH "https://app.franchisesee.com/api/v1/users/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jordan", "last_name": "Rivera", "roles": [ "string" ], "perspective": "admin", "location_id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e" }' ``` ## Responses ### 200 A single User. ```json { "data": { "id": "6e2b9d41-8c7a-4f3e-b1d5-9a0c2e4f6b81", "first_name": "Jordan", "last_name": "Rivera", "email": "jordan.rivera@example.com", "perspective": "admin", "location_id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "has_sso_auth": true, "roles": [ "string" ], "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": {}, "district2": {}, "locationCategory": {}, "locationGroups": [] } ] } ], "location": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ {} ] } ] } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Delete User `DELETE /api/v1/users/{id}` Deletes a User object (and its API tokens). Requires the `delete` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Example request ```bash curl -X DELETE "https://app.franchisesee.com/api/v1/users/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 204 Deleted. ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` --- # Sync Location Groups `PUT /api/v1/users/{id}/location-groups` Replaces the set of location groups a User belongs to, and clears the user's direct `location_id` (group access and direct-location access are mutually exclusive). The `location_group_ids` key must be present; an empty array clears all groups. Requires the `update` ability. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Resource UUID. | ## Request body ```json { "location_group_ids": [ "019be24b-8fb3-7065-bcda-b3a26afdda1f", "019be24b-8fb2-718a-a686-cd8eb8834625" ] } ``` ## Example request ```bash curl -X PUT "https://app.franchisesee.com/api/v1/users/9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63/location-groups" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "location_group_ids": [ "019be24b-8fb3-7065-bcda-b3a26afdda1f", "019be24b-8fb2-718a-a686-cd8eb8834625" ] }' ``` ## Responses ### 200 The updated User, with its `locationGroups` relationship loaded. ```json { "data": { "id": "6e2b9d41-8c7a-4f3e-b1d5-9a0c2e4f6b81", "first_name": "Jordan", "last_name": "Rivera", "email": "jordan.rivera@example.com", "perspective": "admin", "location_id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "has_sso_auth": true, "roles": [ "string" ], "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": {}, "district2": {}, "locationCategory": {}, "locationGroups": [] } ] } ], "location": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "external_id": "STR-1042", "name": "Downtown Austin", "address": "401 Congress Ave", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "timezone": "America/Chicago", "timezone_source": "pos", "is_private": false, "is_corporate": false, "is_active": true, "activation_status": "active", "deactivated_at": null, "is_deletable": false, "deletable_on": null, "district_id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "district_id_2": "c7e9a2d4-1b3f-4e8a-9c6d-5f2e8b1a7d30", "location_category_id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "datasource_settings": { "qu": { "location_id": 12345 }, "brink": { "api_key_set": true } }, "district": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "district2": { "id": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9", "name": "Central Texas", "external_id": "CTX", "district_group_id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "districtGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationCategory": { "id": "5d8e2f1a-6c4b-4a9e-8f3d-2b7c1e9a4d60", "name": "Inline", "external_id": "INLINE", "location_category_group_id": "f2b6d8a1-4c3e-4e7b-a5d9-1c8e3f6b2a40", "sort_order": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locationCategoryGroup": { "id": "e1a4c8b2-7d3f-4b6e-9a2c-8f5d1e3b7a90", "name": "Region", "index": 1, "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z" } }, "locationGroups": [ { "id": "a3c5e7f9-2b4d-4f6a-8c1e-3d5f7a9b1c20", "name": "Austin Metro", "external_id": "AUS-METRO", "created_at": "2025-03-14T15:22:08.000000Z", "updated_at": "2026-10-02T09:41:55.000000Z", "locations": [ {} ] } ] } } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` --- # Get Orders `GET /api/v1/orders` Retrieves a paginated list of Order objects for one location and at most 31 business dates. Every check since the tenant's POS archive began is available: `meta.data_from` is the first business date that can be served, and today's checks are included as they are imported. A request for several locations or a longer range is refused with a 422 pointing at Data Exports, which serves any range asynchronously. Requires the `read` ability and the export orders permission; franchisee tokens may read only locations in their portfolio. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page[number]` | integer | no | Page number (starts at 1). | | `page[size]` | integer | no | Orders per page (default 30, max 1000). | | `filter[location_id]` | string | yes | One location UUID the token may read. | | `filter[start_date]` | string | no | Inclusive lower bound on business date. Defaults to six days before end_date. | | `filter[end_date]` | string | no | Inclusive upper bound on business date. Defaults to today on the tenant's calendar. Must be on or after start_date, and the range at most 31 days. | | `sort` | string | no | Allowed: business_date_on, order_created_at, net_sales_amount. Default: -business_date_on. | | `include` | string | no | Comma-separated. Allowed: items, payments. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/orders?filter%5Blocation_id%5D=9d3f6b2e-5a41-4c7e-9a0b-2f1c8e7d4a63" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "links": { "first": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "last": "https://app.franchisesee.com/api/v1/locations?page[number]=10", "prev": "https://app.franchisesee.com/api/v1/locations?page[number]=1", "next": "https://app.franchisesee.com/api/v1/locations?page[number]=3" }, "meta": { "data_from": "2024-01-01" }, "data": [ { "id": "100245871", "location": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "name": "Downtown Austin" }, "order_created_at": "2026-06-08T10:01:17", "payment_at": "2026-06-08T10:14:02", "business_date_on": "2026-06-08", "check_number": "318", "device_id": "2", "employee_id": "10457", "day_part_id": "2", "channel": { "id": "3", "name": "Online" }, "order_type": { "id": "1", "name": "Pickup" }, "total_amount": 24.87, "tax_amount": 1.89, "tip_amount": 2, "service_charge_amount": 0, "discount_amount": 2, "gift_card_amount": 0, "net_sales_amount": 20.98, "void_amount": 0, "num_customers": 1, "guest_count": 1, "items": [ { "description": "Chicken Burrito Bowl", "item_external_id": "10233", "category_uuid": "b8d1f3a5-7c9e-4b2d-a6f8-0e3c5a7d9b12", "quantity": 1, "price": 11.49, "discount_amount": 1, "net_sales_amount": 10.49, "num_customers": 1 } ], "payments": [ { "tender_type_id": "4", "card_brand": "Visa", "amount": 24.87, "tip_amount": 2, "paid_at": "2026-06-08T10:14:02" } ] } ] } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` ### 503 Order history is busy; retry after the `Retry-After` header's seconds. --- # Get Order `GET /api/v1/orders/{external_id}` Retrieves one Order by its point-of-sale id and business date. A POS id is unique only within a location; pass `location_id` when the token can read several. Requires the `read` ability and the export orders permission. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `external_id` | string | yes | The order's point-of-sale id (the `id` field of an Order). | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `business_date` | string | yes | | | `location_id` | string | no | The location UUID, needed when two locations share the POS id on that date. | | `include` | string | no | Comma-separated. Allowed: items, payments. | ## Example request ```bash curl "https://app.franchisesee.com/api/v1/orders/STORE-1042?business_date=2026-11-12" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" ``` ## Responses ### 200 Success ```json { "data": { "id": "100245871", "location": { "id": "9b1c8f3e-7a2d-4c5b-8e1a-2f3d4c5b6a7e", "name": "Downtown Austin" }, "order_created_at": "2026-06-08T10:01:17", "payment_at": "2026-06-08T10:14:02", "business_date_on": "2026-06-08", "check_number": "318", "device_id": "2", "employee_id": "10457", "day_part_id": "2", "channel": { "id": "3", "name": "Online" }, "order_type": { "id": "1", "name": "Pickup" }, "total_amount": 24.87, "tax_amount": 1.89, "tip_amount": 2, "service_charge_amount": 0, "discount_amount": 2, "gift_card_amount": 0, "net_sales_amount": 20.98, "void_amount": 0, "num_customers": 1, "guest_count": 1, "items": [ { "description": "Chicken Burrito Bowl", "item_external_id": "10233", "category_uuid": "b8d1f3a5-7c9e-4b2d-a6f8-0e3c5a7d9b12", "quantity": 1, "price": 11.49, "discount_amount": 1, "net_sales_amount": 10.49, "num_customers": 1 } ], "payments": [ { "tender_type_id": "4", "card_brand": "Visa", "amount": 24.87, "tip_amount": 2, "paid_at": "2026-06-08T10:14:02" } ] }, "meta": { "data_from": "2024-01-01" } } ``` ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 404 Resource not found. ```json { "message": "Record not found." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ``` ### 503 Order history is busy; retry after the `Retry-After` header's seconds. --- # Submit Custom Metric Values `POST /api/custom-metrics` Upserts daily custom-metric values for one or more locations, keyed by the location's `external_id`. Locations not found for the tenant are skipped (logged, not errored). Requires the `create` ability. ## Request body ```json { "metric": "custom:loyalty_signups", "businessDateOn": "2026-06-01", "locationValues": [ { "locationId": 1024, "value": 37 }, { "locationId": 1025, "value": 12 } ] } ``` ## Example request ```bash curl -X POST "https://app.franchisesee.com/api/custom-metrics" \ -H "Authorization: Bearer $FRANCHISESEE_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "metric": "custom:loyalty_signups", "businessDateOn": "2026-06-01", "locationValues": [ { "locationId": 1024, "value": 37 }, { "locationId": 1025, "value": 12 } ] }' ``` ## Responses ### 200 Values accepted and upserted. Empty body. ### 401 Authentication failed or missing. ```json { "message": "Unauthenticated." } ``` ### 403 The token lacks the required ability or the caller is not permitted. ```json { "message": "This API token does not have the 'create' ability." } ``` ### 422 Validation failed. ```json { "message": "The name field is required.", "errors": { "name": [ "The name field is required." ] } } ```