{"openapi": "3.1.0", "info": {"title": "API Documentation", "version": "1.2.0", "description": "# Introduction\n\nProgrammatic access to your Sellfy store from your own software.\nEndpoints are meant to be called from your server or application,\nand new ones are added regularly.\n\n# Authentication\n\nEvery request is authenticated with an API token \u2014 a secret tied to\nyour Sellfy account, so a request only ever sees your own data.\n\nTo get a token:\n\n1. In the Sellfy dashboard, open **Store \u2192 Integrations \u2192 API keys**.\n2. Create a token and copy it right away \u2014 the full value is shown\n   only once, on creation.\n3. Send it with every request in the `Authorization` header:\n\n```\nAuthorization: Bearer <your token>\n```\n\nRequests without a valid token are answered with `401` and a `code`\nof `token_missing` or `token_invalid`. A token can be deleted in the\ndashboard at any time to cut an integration off."}, "servers": [{"url": "https://api.sellfy.com"}], "security": [{"apiToken": []}], "tags": [{"name": "Ping", "description": "Verify that your integration can reach the API.", "x-order": 1}, {"name": "Licenses", "description": "Validate, activate and revoke license keys sold with Sellfy products. A key is located by its `product_id` and `license_key` pair within your account.", "x-order": 2}, {"name": "MCP server", "description": "Sellfy runs an MCP (Model Context Protocol) server, so tools like\nClaude can manage your store in plain language. Point any MCP\nclient that supports remote servers (Streamable HTTP) at:\n\n```\nhttps://api.sellfy.com/mcp\n```\n\n## Let your agent set it up\n\nPaste this into Claude Code, Codex, Cursor, VS Code or Gemini CLI:\n\n```\nSet up the Sellfy MCP server for me by following https://api.sellfy.com/agent-setup.md\n```\n\n## Connect a client\n\nEvery client goes through the same three steps: add the server URL,\nsign in once (a browser window opens on Sellfy, where you approve\nthe access), and re-authorize when Sellfy asks for it again.\nClients register themselves, so there is nothing to set up in\nSellfy first.\n\n**Claude** (claude.ai or the desktop app)\n\n- Add: Settings \u2192 Connectors \u2192 Browse connectors, find Sellfy and\n  click Connect. If it is not listed for your organization, choose\n  Add custom connector and paste the URL above.\n- Sign in: click Connect on the Sellfy connector and approve in the\n  browser.\n- Re-authorize: Settings \u2192 Connectors \u2192 Sellfy \u2192 Disconnect, then\n  Connect again.\n\n**Claude Code**\n\n```\nclaude mcp add --transport http sellfy https://api.sellfy.com/mcp\n```\n\n- Sign in: inside a session run `/mcp`, pick `sellfy` and choose\n  Authenticate; the browser opens on Sellfy.\n- Re-authorize: `/mcp` \u2192 `sellfy` \u2192 Clear authentication, then\n  Authenticate again. Or remove and re-add the server:\n\n```\nclaude mcp remove sellfy\nclaude mcp add --transport http sellfy https://api.sellfy.com/mcp\n```\n\n**Codex CLI**\n\n```\ncodex mcp add sellfy --url https://api.sellfy.com/mcp\ncodex mcp login sellfy\n```\n\n- `codex mcp login` opens the browser on Sellfy; approve the access\n  and return to the terminal. `codex mcp get sellfy` shows the\n  connection.\n- Re-authorize:\n\n```\ncodex mcp logout sellfy\ncodex mcp login sellfy\n```\n\n**ChatGPT** and **Codex app**\n\n- Add: open the plugin directory, find Sellfy and choose Connect.\n  Without the directory entry, open chatgpt.com/plugins, click +\n  \u2192 Add custom MCP server, paste the URL above with OAuth as the\n  authentication, then Create as a plugin.\n- Sign in: ChatGPT opens Sellfy in the browser when you first use\n  the connector.\n- Re-authorize: remove the connector and create it again.\n\n**Cursor** \u2014 add to `.cursor/mcp.json`:\n\n```\n{\"mcpServers\": {\"sellfy\": {\"url\": \"https://api.sellfy.com/mcp\"}}}\n```\n\n- Sign in: Cursor Settings \u2192 MCP shows `sellfy` with a Login\n  button; click it and approve in the browser.\n- Re-authorize: the same MCP settings entry offers Logout, then\n  Login again.\n\n**VS Code** \u2014 add to `.vscode/mcp.json`:\n\n```\n{\"servers\": {\"sellfy\": {\"type\": \"http\", \"url\": \"https://api.sellfy.com/mcp\"}}}\n```\n\n- Sign in: VS Code prompts to sign in when the server starts; the\n  browser opens on Sellfy.\n- Re-authorize: Command Palette \u2192 `MCP: List Servers` \u2192 `sellfy`\n  \u2192 Sign out, then start the server again.\n\nAny other OAuth-capable MCP client connects the same way: give it\nthe URL, then sign in when it asks.\n\n## Authentication\n\nThe MCP server does not use the API tokens described above. A\nclient connects through OAuth: on the first connection you sign in\nto Sellfy in the browser and approve or refuse the access it\nrequests \u2014 `mcp:read` covers the read tools, `mcp:write` the write\ntools.\n\nA connection stays valid while the client keeps using it: the\nclient renews its access silently, and a connection unused for 30\ndays, or older than 180 days, expires. To end a connection,\ndisconnect the server in the client; changing or resetting your\nSellfy password ends every connection at once. In each of those\ncases the client reports an authorization error on its next call;\nre-authorize it as described for your client above to continue.\n\n## What you can ask\n\nThe assistant works only inside a conversation you start; nothing\nruns in the background. Some prompts that work as written:\n\n**Products**\n\n- \"Create a digital product called Autumn Presets at 19.99, keep it\n  unpublished for now.\"\n- \"Attach this file to Autumn Presets and publish it.\"\n- \"Set a compare-at price of 29.99 on every product in the presets\n  category.\"\n\n**Sales and customers**\n\n- \"How did last week compare with the week before?\"\n- \"Which five products earned the most this month?\"\n- \"Show the orders from jane@example.com.\"\n\n**Marketing**\n\n- \"Create a 20 percent discount code NEWSLETTER valid until Friday.\"\n- \"Draft an email campaign announcing Autumn Presets and send me a\n  test.\"\n- \"List the reviews from the last 30 days with fewer than four\n  stars.\"\n\n**Storefront**\n\n- \"Start a new draft of my store, add a page for the course bundle,\n  and give me a preview link.\"\n- \"Publish the draft.\"\n\n**License keys**\n\n- \"Add these license keys to Autumn Presets.\"\n- \"Revoke the key ending in 7F3A.\"\n\n## Tools\n\n### Store\n\n| Tool | What it does | Access |\n|---|---|---|\n| `get_store_info` | Store overview | read |\n\n### Products\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_products` | List products | read |\n| `get_product` | Get product | read |\n| `create_product` | Create product | write |\n| `get_product_contents` | Get product contents | read |\n| `set_product_contents` | Replace product contents | write |\n| `begin_product_upload` | Begin file upload | write |\n| `finalize_product_upload` | Finalize file upload | write |\n| `update_product` | Update product | write |\n\n### Variants\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_product_variants` | List product variants | read |\n| `set_product_variants` | Set product variants | write |\n\n### Media\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_product_media` | List product media | read |\n| `add_product_media_link` | Add product embed | write |\n| `begin_product_image_upload` | Start product image upload | write |\n| `finalize_product_image_upload` | Finish product image upload | write |\n| `set_product_media` | Set product media | write |\n\n### Shipping\n\n| Tool | What it does | Access |\n|---|---|---|\n| `get_product_shipping` | Get product shipping | read |\n| `set_product_shipping` | Set product shipping | write |\n| `list_shipping_profiles` | List shipping profiles | read |\n| `get_shipping_profile` | Get shipping profile | read |\n| `create_shipping_profile` | Create shipping profile | write |\n| `update_shipping_profile` | Update shipping profile | write |\n\n### Licensing\n\n| Tool | What it does | Access |\n|---|---|---|\n| `get_product_licensing` | Get product licensing | read |\n| `set_product_licensing` | Set product licensing | write |\n| `add_license_keys` | Add license keys | write |\n| `list_license_keys` | List license keys | read |\n| `revoke_license_key` | Revoke license key | write |\n| `reactivate_license_key` | Reactivate license key | write |\n| `delete_license_key` | Delete license key | write |\n\n### Orders\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_orders` | Search orders | read |\n| `get_order` | Get order | read |\n\n### Analytics\n\n| Tool | What it does | Access |\n|---|---|---|\n| `get_sales_summary` | Sales summary | read |\n| `get_sales_histogram` | Sales over time | read |\n| `get_top_stats` | Top sellers, sources and countries | read |\n\n### Discounts\n\n| Tool | What it does | Access |\n|---|---|---|\n| `create_discount` | Create discount | write |\n| `list_discounts` | List discounts | read |\n| `update_discount` | Update discount | write |\n\n### Campaigns\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_email_campaigns` | List email campaigns | read |\n| `create_email_campaign` | Create email campaign draft | write |\n| `update_email_campaign` | Update email campaign | write |\n| `send_test_email` | Send test email | write |\n\n### Customers\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_customers` | List customers | read |\n\n### Reviews\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_reviews` | List reviews | read |\n\n### Store Pages\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_store_pages` | List store pages | read |\n| `get_store_page` | Get store page | read |\n| `create_store_page` | Create store page | write |\n| `update_store_page` | Update store page | write |\n\n### Store Modules\n\n| Tool | What it does | Access |\n|---|---|---|\n| `list_store_module_types` | List store block types | read |\n| `set_store_module` | Set store block | write |\n\n### Store Settings\n\n| Tool | What it does | Access |\n|---|---|---|\n| `get_store_settings` | Get store settings | read |\n| `update_store_settings` | Update store settings | write |\n\n### Store Versions\n\n| Tool | What it does | Access |\n|---|---|---|\n| `create_store_version` | Create store draft | write |\n| `list_store_versions` | List store versions | read |\n| `publish_store_version` | Publish store draft | write |\n| `restore_store_version` | Restore store backup | write |\n| `delete_store_version` | Delete store draft | write |\n\n## What the server will and will not do\n\n- New products are created unpublished. Publishing is a separate,\n  explicit step, and a digital product needs a file, text, link or\n  license keys before it can be published.\n- Email campaigns are drafts only. The assistant can send a test to\n  your own address; sending to your list happens in the Sellfy\n  dashboard.\n- There are no tools to delete products or discounts, issue\n  refunds, or move money.\n- Storefront edits carry a revision. If the store changed in the\n  meantime (for example you edited it in the dashboard), the write\n  is rejected and the assistant reads again and retries.\n- Prices you give are taken in your store currency, as set in the\n  dashboard. Say \"19.99\" and the product is priced 19.99 in that\n  currency; the assistant does not convert between currencies.\n- Plan limits apply exactly as in the dashboard. When a feature is\n  not included in your plan, the tool says so; the plans are\n  described at https://sellfy.com/pricing/.\n- Requests are limited to 120 per minute across all of\n  your connected clients. Beyond that the server answers with a\n  retry delay, and the assistant waits.\n\n## Privacy\n\nThrough these tools the assistant can read your products, orders,\ncustomers, reviews, campaigns and storefront content. Orders,\ncustomers and reviews include buyer names and email addresses.\nEverything a tool returns is sent to the AI provider whose client\nyou connected (for example Anthropic or OpenAI) and handled under\nthat provider's terms. Sellfy sends nothing to a provider outside a\nconversation you start. Sellfy's own handling of this data is\ndescribed in the privacy policy at https://sellfy.com/privacy/.\n\n## Security\n\nWrite tools change the live store. Keep your client's confirmation\nprompt on for write tools, and avoid running the Sellfy server in\nthe same agent session as untrusted MCP servers: a malicious tool\nresult there can try to steer the agent into unwanted writes\n(prompt injection).\n\n## Troubleshooting\n\n- **Authorization error after the client updated or after a\n  password change**: re-authorize as described for your client\n  above.\n- **The URL with my store domain does not work**: the server is\n  always at the URL at the top of this page, never at your store\n  address or custom domain.\n- **\"Stale revision\" on a storefront write**: the store changed\n  since the assistant last read it. Ask it to reload and try again.\n- **A tool says the feature is not on my plan**: the same limit\n  applies in the dashboard; see the pricing page above.\n\n## Support\n\nEmail support@sellfy.com with the client you use and, if there is\none, the error text the assistant showed."}], "paths": {"/ping/": {"get": {"tags": ["Ping"], "operationId": "ping", "summary": "Ping the API", "description": "Answers `pong` when the request is well-formed and the API token is\nvalid \u2014 a harmless call to verify an integration's setup before\ngoing live.", "responses": {"200": {"description": "The token is valid and the API is reachable.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PingResponse"}}}}, "401": {"description": "The API token is missing or invalid (`code`: `token_missing` or `token_invalid`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/licenses/validate/": {"get": {"tags": ["Licenses"], "operationId": "validate-license", "summary": "Validate a license key", "description": "Looks up the current state of a license key. Read-only \u2014 validating\nnever changes the key.", "parameters": [{"name": "product_id", "in": "query", "required": true, "description": "ID of the Sellfy product the license key belongs to. To find it, open **Products \u2192 All products** in your dashboard and pick **Copy product ID** from the options menu (\u22ef) of the product row. Alternatively, open the product for editing and copy the ID from the address bar: it is the last part of the URL", "schema": {"title": "Product Id", "type": "string"}}, {"name": "license_key", "in": "query", "required": true, "description": "The license key string.", "schema": {"minLength": 1, "title": "License Key", "type": "string"}}], "responses": {"200": {"description": "Current state of the license key.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LicenseStateResponse"}}}}, "400": {"description": "The request is missing or has invalid parameters.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ValidationError"}}}}, "401": {"description": "The API token is missing or invalid (`code`: `token_missing` or `token_invalid`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "404": {"description": "No license key of yours matches `product_id` and `license_key` (`code`: `not_found`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "429": {"description": "Too many failed lookups from this IP (`code`: `too_many_attempts`). More than 30 lookups of non-existent keys block further requests for 15 minutes; successful lookups are never throttled.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/licenses/activations/": {"post": {"tags": ["Licenses"], "operationId": "activate-license", "summary": "Activate a license key", "description": "Records an activation on the key and returns just that activation.\nStore its `id` \u2014 deactivating later is\n`DELETE /v1/licenses/activations/{activation_id}/`.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LicenseActivateInput"}}, "application/x-www-form-urlencoded": {"schema": {"$ref": "#/components/schemas/LicenseActivateInput"}}}}, "responses": {"200": {"description": "A single activation recorded on the license key.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LicenseActivationResponse"}}}}, "400": {"description": "The request is missing or has invalid parameters.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ValidationError"}}}}, "401": {"description": "The API token is missing or invalid (`code`: `token_missing` or `token_invalid`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "403": {"description": "The key exists but is not valid (`code`: `revoked` or `expired`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "404": {"description": "No license key of yours matches `product_id` and `license_key` (`code`: `not_found`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "429": {"description": "Too many failed lookups from this IP (`code`: `too_many_attempts`). More than 30 lookups of non-existent keys block further requests for 15 minutes; successful lookups are never throttled.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/licenses/activations/{activation_id}/": {"delete": {"tags": ["Licenses"], "operationId": "deactivate-license", "summary": "Deactivate a license key activation", "description": "Releases an activation recorded earlier, e.g. when the buyer moves the\nlicense to another device, and returns the key's remaining state. The\nactivation `id` alone addresses it \u2014 activation ids are unique within\nyour account.", "parameters": [{"name": "activation_id", "in": "path", "required": true, "description": "Id of the activation to release, as returned when activating.", "schema": {"type": "string"}}], "responses": {"200": {"description": "Current state of the license key.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LicenseStateResponse"}}}}, "401": {"description": "The API token is missing or invalid (`code`: `token_missing` or `token_invalid`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "404": {"description": "No activation of yours has this id (`code`: `activation_not_found`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/licenses/revoke/": {"post": {"tags": ["Licenses"], "operationId": "revoke-license", "summary": "Revoke a license key", "description": "Marks the key as revoked so it can no longer be activated \u2014 the same\naction as revoking the key in the Sellfy dashboard, where it can also\nbe reactivated. Idempotent \u2014 revoking an already-revoked key just\nreturns its state.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LicenseLookupInput"}}, "application/x-www-form-urlencoded": {"schema": {"$ref": "#/components/schemas/LicenseLookupInput"}}}}, "responses": {"200": {"description": "Current state of the license key.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LicenseStateResponse"}, "example": {"valid": false, "status": "revoked", "activations": []}}}}, "400": {"description": "The request is missing or has invalid parameters.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ValidationError"}}}}, "401": {"description": "The API token is missing or invalid (`code`: `token_missing` or `token_invalid`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "404": {"description": "No license key of yours matches `product_id` and `license_key` (`code`: `not_found`).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "429": {"description": "Too many failed lookups from this IP (`code`: `too_many_attempts`). More than 30 lookups of non-existent keys block further requests for 15 minutes; successful lookups are never throttled.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}}, "components": {"schemas": {"LicenseActivateInput": {"properties": {"product_id": {"description": "ID of the Sellfy product the license key belongs to. To find it, open **Products \u2192 All products** in your dashboard and pick **Copy product ID** from the options menu (\u22ef) of the product row. Alternatively, open the product for editing and copy the ID from the address bar: it is the last part of the URL", "title": "Product Id", "type": "string"}, "license_key": {"description": "The license key string.", "minLength": 1, "title": "License Key", "type": "string"}, "meta": {"default": {}, "description": "Free-form data describing the activation (e.g. device id, site url). Stored as-is and returned with the activation.", "title": "Meta", "type": "object"}}, "required": ["product_id", "license_key"], "title": "LicenseActivateInput", "type": "object"}, "LicenseActivationResponse": {"description": "A single activation recorded on the license key.", "properties": {"id": {"description": "Id of the activation \u2014 deactivate it with `DELETE /v1/licenses/activations/{activation_id}/`.", "title": "Id", "type": "string"}, "activated_at": {"title": "Activated At", "type": "string"}, "meta": {"description": "The `meta` data sent when activating.", "readOnly": true, "title": "Meta", "type": "object"}}, "required": ["id", "activated_at", "meta"], "title": "LicenseActivationResponse", "type": "object"}, "LicenseLookupInput": {"properties": {"product_id": {"description": "ID of the Sellfy product the license key belongs to. To find it, open **Products \u2192 All products** in your dashboard and pick **Copy product ID** from the options menu (\u22ef) of the product row. Alternatively, open the product for editing and copy the ID from the address bar: it is the last part of the URL", "title": "Product Id", "type": "string"}, "license_key": {"description": "The license key string.", "minLength": 1, "title": "License Key", "type": "string"}}, "required": ["product_id", "license_key"], "title": "LicenseLookupInput", "type": "object"}, "LicenseStateResponse": {"description": "Current state of the license key.", "properties": {"valid": {"description": "Whether the key is currently usable.", "readOnly": true, "title": "Valid", "type": "boolean"}, "status": {"description": "`issued` (sold, not activated yet), `activated` (has at least one activation), or the reason the key is not valid: `revoked` or `expired`.", "readOnly": true, "title": "Status", "type": "string"}, "activations": {"description": "Currently active activations, oldest first.", "items": {"$ref": "#/components/schemas/LicenseActivationResponse"}, "readOnly": true, "title": "Activations", "type": "array"}}, "required": ["valid", "status", "activations"], "title": "LicenseStateResponse", "type": "object"}, "PingResponse": {"description": "The token is valid and the API is reachable.", "properties": {"message": {"const": "pong", "default": "pong", "enum": ["pong"], "title": "Message", "type": "string"}}, "title": "PingResponse", "type": "object"}, "Error": {"type": "object", "description": "Error envelope with a machine-readable reason code.", "properties": {"code": {"type": "string"}}, "required": ["code"]}, "ValidationError": {"type": "object", "description": "Request validation failure.", "properties": {"error": {"type": "string", "description": "Message of the first validation error."}, "errors": {"type": "array", "items": {"type": "object", "properties": {"label": {"type": "string", "description": "Dotted path of the invalid field."}, "message": {"type": "string"}}}}}}}, "securitySchemes": {"apiToken": {"type": "http", "scheme": "bearer", "description": "API token created in the Sellfy dashboard. The full value is shown only once, on creation."}}}}