Manage OpenRouter keys, conversations, and MCP tool execution
/admin/api/ai/key
Check whether the authenticated user has a personal OpenRouter API key, and whether the shop has a shared OpenRouter integration key configured.
/admin/api/ai/key
Save or clear the authenticated user's personal OpenRouter API key. Pass null (or omit api_key) to clear. The key is encrypted with AES-256-GCM before being stored.
/admin/api/ai/key/resolve
Resolve the effective OpenRouter API key and default model for the authenticated user. Tries the personal key first, then falls back to the shop-wide OpenRouter integration key. Used by the chat UI and inline-AI hooks to issue completions without a second round-trip.
/admin/api/ai/conversations
List all AI conversations owned by the authenticated user, most-recently-updated first. Conversations inactive for more than 30 days are auto-purged on list.
/admin/api/ai/conversations
Create a new AI conversation. Title defaults to "New conversation" when absent or blank.
/admin/api/ai/conversations/{id}
Get a single conversation with all its messages in chronological order.
/admin/api/ai/conversations/{id}
Rename a conversation.
/admin/api/ai/conversations/{id}
Delete a conversation and all its messages.
/admin/api/ai/conversations/{id}/messages
Append one or more messages to an existing conversation. Typically called after a chat turn to persist the user message and the assistant's reply together.
/admin/api/ai/tools
List all MCP tools available to the AI assistant in OpenAI function-calling format. Each tool is scoped to the user's role permissions. Schemas are pre-cleaned for OpenAI and Anthropic compatibility.
/admin/api/ai/tool-execute
Execute a named MCP tool and return its text output. Tool name normalization is automatic — uppercase names, dotted prefixes, or non-alphanumeric characters are stripped during match. String "true"/"false" values in arguments are coerced to booleans.
/admin/api/ai/upload
Upload an HTML file or ZIP archive for the AI assistant to read. Single HTML files up to 500 KB are returned as-is. ZIP archives up to 5 MB are scanned; all .html/.htm/.css/.js entries are extracted. Returned files are typically passed back to the assistant as context for theme-porting conversations.
/admin/api/ai/key
Check whether the authenticated user has a personal OpenRouter API key, and whether the shop has a shared OpenRouter integration key configured.
{
"has_key": true,
"has_shop_key": false
}
curl "https://yourshop.zeroshop.io/admin/api/ai/key" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/ai/key",
headers={"Authorization": "Bearer zspat_..."}
)
status = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/key",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const status = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/key")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
status = JSON.parse(resp.body)
/admin/api/ai/key
Save or clear the authenticated user's personal OpenRouter API key. Pass null (or omit api_key) to clear. The key is encrypted with AES-256-GCM before being stored.
{
"api_key": "sk-or-v1-..."
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/ai/key" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"api_key": "sk-or-v1-..."}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/ai/key",
headers={"Authorization": "Bearer zspat_..."},
json={"api_key": "sk-or-v1-..."}
)
# 204 No Content on success
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/key",
{
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ api_key: "sk-or-v1-..." })
}
);
// 204 No Content on success
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/key")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { api_key: "sk-or-v1-..." }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# 204 No Content on success
/admin/api/ai/key/resolve
Resolve the effective OpenRouter API key and default model for the authenticated user. Tries the personal key first, then falls back to the shop-wide OpenRouter integration key. Used by the chat UI and inline-AI hooks to issue completions without a second round-trip.
{
"api_key": "sk-or-v1-...",
"model": "openai/gpt-4o-mini",
"system_prompt": ""
}
curl "https://yourshop.zeroshop.io/admin/api/ai/key/resolve" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/ai/key/resolve",
headers={"Authorization": "Bearer zspat_..."}
)
data = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/key/resolve",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const { api_key, model } = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/key/resolve")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/ai/conversations
List all AI conversations owned by the authenticated user, most-recently-updated first. Conversations inactive for more than 30 days are auto-purged on list.
[
{
"id": "f3b2c1e4-1234-4abc-9def-0123456789ab",
"owner_id": 42,
"title": "Pricing strategy ideas",
"created_at": "2026-04-10T10:00:00Z",
"updated_at": "2026-04-15T14:30:00Z"
},
{
"id": "a1b2c3d4-5678-4def-9abc-fedcba987654",
"owner_id": 42,
"title": "New conversation",
"created_at": "2026-04-08T09:00:00Z",
"updated_at": "2026-04-08T09:15:00Z"
}
]
curl "https://yourshop.zeroshop.io/admin/api/ai/conversations" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/ai/conversations",
headers={"Authorization": "Bearer zspat_..."}
)
conversations = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/conversations",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const conversations = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/conversations")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/ai/conversations
Create a new AI conversation. Title defaults to "New conversation" when absent or blank.
{
"title": "Pricing strategy ideas"
}
{
"id": "f3b2c1e4-1234-4abc-9def-0123456789ab"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/ai/conversations" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"title": "Pricing strategy ideas"}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/ai/conversations",
headers={"Authorization": "Bearer zspat_..."},
json={"title": "Pricing strategy ideas"}
)
conversation_id = resp.json()["id"]
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/conversations",
{
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ title: "Pricing strategy ideas" })
}
);
const { id } = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/conversations")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { title: "Pricing strategy ideas" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/ai/conversations/{id}
Get a single conversation with all its messages in chronological order.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | required | Conversation UUID |
{
"id": "f3b2c1e4-1234-4abc-9def-0123456789ab",
"owner_id": 42,
"title": "Pricing strategy ideas",
"created_at": "2026-04-10T10:00:00Z",
"updated_at": "2026-04-15T14:30:00Z",
"messages": [
{
"id": "msg-uuid-1",
"conversation_id": "f3b2c1e4-1234-4abc-9def-0123456789ab",
"role": "user",
"content": "Should I run a Black Friday sale this year?",
"token_count": 12,
"created_at": "2026-04-10T10:00:05Z"
},
{
"id": "msg-uuid-2",
"conversation_id": "f3b2c1e4-1234-4abc-9def-0123456789ab",
"role": "assistant",
"content": "Based on last year's orders, yes — your Q4 revenue peaked...",
"token_count": 245,
"created_at": "2026-04-10T10:00:10Z"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab",
headers={"Authorization": "Bearer zspat_..."}
)
conversation = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const conversation = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/ai/conversations/{id}
Rename a conversation.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | required | Conversation UUID |
{
"title": "Q4 promo planning"
}
curl -X PATCH "https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"title": "Q4 promo planning"}'
import requests
resp = requests.patch(
"https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab",
headers={"Authorization": "Bearer zspat_..."},
json={"title": "Q4 promo planning"}
)
# 204 No Content on success
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab",
{
method: "PATCH",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ title: "Q4 promo planning" })
}
);
// 204 No Content on success
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { title: "Q4 promo planning" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# 204 No Content on success
/admin/api/ai/conversations/{id}
Delete a conversation and all its messages.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | required | Conversation UUID |
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab",
{
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
}
);
// 204 No Content
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# 204 No Content
/admin/api/ai/conversations/{id}/messages
Append one or more messages to an existing conversation. Typically called after a chat turn to persist the user message and the assistant's reply together.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | required | Conversation UUID |
{
"messages": [
{
"role": "user",
"content": "Summarise today's orders.",
"token_count": 8
},
{
"role": "assistant",
"content": "You had 12 orders today totalling $2,345...",
"token_count": 180
}
]
}
{
"message_ids": [
"msg-uuid-1",
"msg-uuid-2"
]
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab/messages" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Hi", "token_count": 1}]}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab/messages",
headers={"Authorization": "Bearer zspat_..."},
json={"messages": [{"role": "user", "content": "Hi", "token_count": 1}]}
)
message_ids = resp.json()["message_ids"]
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab/messages",
{
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
messages: [{ role: "user", content: "Hi", token_count: 1 }]
})
}
);
const { message_ids } = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/conversations/f3b2c1e4-1234-4abc-9def-0123456789ab/messages")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { messages: [{ role: "user", content: "Hi", token_count: 1 }] }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/ai/tools
List all MCP tools available to the AI assistant in OpenAI function-calling format. Each tool is scoped to the user's role permissions. Schemas are pre-cleaned for OpenAI and Anthropic compatibility.
[
{
"type": "function",
"function": {
"name": "list_products",
"description": "List products in the shop, optionally filtered by category or search term.",
"parameters": {
"type": "object",
"properties": {
"search": {
"type": "string",
"description": "Optional case-insensitive search on product name"
},
"category_id": {
"type": "integer",
"description": "Optional filter by category ID"
}
}
}
}
}
]
curl "https://yourshop.zeroshop.io/admin/api/ai/tools" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/ai/tools",
headers={"Authorization": "Bearer zspat_..."}
)
tools = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/tools",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const tools = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/tools")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
tools = JSON.parse(resp.body)
/admin/api/ai/tool-execute
Execute a named MCP tool and return its text output. Tool name normalization is automatic — uppercase names, dotted prefixes, or non-alphanumeric characters are stripped during match. String "true"/"false" values in arguments are coerced to booleans.
{
"tool_name": "list_products",
"arguments": {
"search": "winter"
}
}
{
"result": "Found 3 products matching 'winter':\n- Winter Jacket ($89.99)\n- Winter Boots ($129.99)\n- Winter Scarf ($24.99)",
"is_error": false
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/ai/tool-execute" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"tool_name": "list_products", "arguments": {"search": "winter"}}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/ai/tool-execute",
headers={"Authorization": "Bearer zspat_..."},
json={"tool_name": "list_products", "arguments": {"search": "winter"}}
)
data = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/tool-execute",
{
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
tool_name: "list_products",
arguments: { search: "winter" }
})
}
);
const { result, is_error } = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/tool-execute")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { tool_name: "list_products", arguments: { search: "winter" } }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/ai/upload
Upload an HTML file or ZIP archive for the AI assistant to read. Single HTML files up to 500 KB are returned as-is. ZIP archives up to 5 MB are scanned; all .html/.htm/.css/.js entries are extracted. Returned files are typically passed back to the assistant as context for theme-porting conversations.
| Name | Type | Required | Description |
|---|---|---|---|
| file | file | required | HTML file (max 500 KB) or ZIP archive (max 5 MB). |
{
"files": [
{
"filename": "index.html",
"content": "<!DOCTYPE html><html>..."
},
{
"filename": "style.css",
"content": "body { font-family: sans-serif; }"
}
]
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/ai/upload" \
-H "Authorization: Bearer zspat_..." \
-F "file=@theme.zip"
import requests
with open("theme.zip", "rb") as f:
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/ai/upload",
headers={"Authorization": "Bearer zspat_..."},
files={"file": f}
)
files = resp.json()["files"]
const form = new FormData();
form.append("file", fileInput.files[0]);
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/ai/upload",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." },
body: form
}
);
const { files } = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/ai/upload")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
form = [["file", File.open("theme.zip")]]
req.set_form(form, "multipart/form-data")
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
We value your privacy
We use cookies for essential site functionality and, with your consent, analytics to understand how our platform is used. No personal data is shared with third parties. See our Privacy Policy for details.