Manage team members, roles, and permissions
/admin/api/me
Returns the current authenticated user's info and effective permissions. Owners get all permission codes.
/admin/api/permissions
List all available permission codes with their groups and descriptions. Any authenticated admin can access this.
/admin/api/team
List admin users with pagination, search, role filtering, and sorting.
/admin/api/team
Invite (create) a new admin user. The password is hashed with Argon2 before storage.
/admin/api/team/{id}
Get a single admin user by ID.
/admin/api/team/{id}
Update an admin user's name and role assignment.
/admin/api/team/{id}
Remove an admin user. You cannot delete your own account.
/admin/api/roles
List all roles with their assigned permissions.
/admin/api/roles
Create a new role with a set of permissions.
/admin/api/roles/{id}
Get a single role with its permissions.
/admin/api/roles/{id}
Update a role's name and permissions. Replaces the entire permission set.
/admin/api/roles/{id}
Delete a role. Users assigned this role are promoted to Owner (role_id set to NULL).
/admin/api/personal-access-tokens
List the caller's personal access tokens. Filtered to the authenticated user -- you can never see another user's tokens.
/admin/api/personal-access-tokens
Create a new personal access token. Cookie-only -- PAT-authenticated callers receive 403. The plaintext token is returned exactly once. Max 20 active tokens per user.
/admin/api/personal-access-tokens/{id}
Revoke a personal access token. You can only revoke your own tokens -- attempts to revoke another user's token return 404.
/admin/api/me
Returns the current authenticated user's info and effective permissions. Owners get all permission codes.
{
"id": 1,
"email": "owner@myshop.com",
"name": "Jane Smith",
"is_owner": true,
"role_id": null,
"role_name": null,
"permissions": [
"dashboard.view",
"products.view",
"products.create",
"products.edit",
"products.delete",
"orders.view",
"orders.edit",
"settings.view",
"settings.edit",
"users.view",
"users.create",
"users.edit",
"users.delete",
"roles.view",
"roles.create",
"roles.edit",
"roles.delete",
"subscriptions.view",
"subscriptions.edit"
]
}
curl "https://yourshop.zeroshop.io/admin/api/me" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/me",
headers={"Authorization": "Bearer zspat_..."}
)
me = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/me",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const me = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/me")
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/permissions
List all available permission codes with their groups and descriptions. Any authenticated admin can access this.
[
{
"code": "dashboard.view",
"group": "Dashboard",
"description": "View dashboard metrics"
},
{
"code": "products.view",
"group": "Products",
"description": "View products"
},
{
"code": "products.create",
"group": "Products",
"description": "Create products"
},
{
"code": "orders.view",
"group": "Orders",
"description": "View orders"
},
{
"code": "settings.view",
"group": "Settings",
"description": "View settings"
},
{
"code": "settings.edit",
"group": "Settings",
"description": "Edit settings"
}
]
curl "https://yourshop.zeroshop.io/admin/api/permissions" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/permissions",
headers={"Authorization": "Bearer zspat_..."}
)
permissions = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/permissions",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const permissions = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/permissions")
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/team
List admin users with pagination, search, role filtering, and sorting.
| Name | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number, 1-indexed (default: 1) |
| per_page | integer | optional | Items per page, 1-100 (default: 20) |
| search | string | optional | Text search on name or email |
| role | string | optional | Filter by role: "owner" for owners, or a numeric role ID |
| sort | string | optional | Sort field with optional - prefix for descending. Valid: name, email, created_at. Default: created_at |
{
"data": [
{
"id": 1,
"email": "owner@myshop.com",
"name": "Jane Smith",
"is_owner": true,
"role_id": null,
"role_name": null,
"created_at": "2025-10-01T10:00:00Z"
},
{
"id": 2,
"email": "editor@myshop.com",
"name": "Bob Jones",
"is_owner": false,
"role_id": 1,
"role_name": "Editor",
"created_at": "2025-11-05T14:00:00Z"
}
],
"page": 1,
"per_page": 20,
"total": 2,
"total_pages": 1
}
curl "https://yourshop.zeroshop.io/admin/api/team?page=1&per_page=20&sort=-created_at" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/team",
params={"page": 1, "per_page": 20, "sort": "-created_at"},
headers={"Authorization": "Bearer zspat_..."}
)
team = resp.json()
const params = new URLSearchParams({ page: 1, per_page: 20, sort: "-created_at" });
const resp = await fetch(
`https://yourshop.zeroshop.io/admin/api/team?${params}`,
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const team = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/team?page=1&per_page=20&sort=-created_at")
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/team
Invite (create) a new admin user. The password is hashed with Argon2 before storage.
{
"email": "newadmin@myshop.com",
"name": "Alice Lee",
"password": "securePass123!",
"role_id": 1
}
{
"id": 3,
"email": "newadmin@myshop.com",
"name": "Alice Lee",
"is_owner": false,
"role_id": 1,
"role_name": "Editor",
"created_at": "2025-11-20T14:00:00Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/team" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"email": "newadmin@myshop.com", "name": "Alice Lee", "password": "securePass123!", "role_id": 1}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/team",
headers={"Authorization": "Bearer zspat_..."},
json={
"email": "newadmin@myshop.com",
"name": "Alice Lee",
"password": "securePass123!",
"role_id": 1
}
)
member = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/team",
{
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
email: "newadmin@myshop.com",
name: "Alice Lee",
password: "securePass123!",
role_id: 1
})
}
);
const member = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/team")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { email: "newadmin@myshop.com", name: "Alice Lee", password: "securePass123!", role_id: 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/team/{id}
Get a single admin user by ID.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Admin user ID |
{
"id": 2,
"email": "editor@myshop.com",
"name": "Bob Jones",
"is_owner": false,
"role_id": 1,
"role_name": "Editor",
"created_at": "2025-11-05T14:00:00Z"
}
curl "https://yourshop.zeroshop.io/admin/api/team/2" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/team/2",
headers={"Authorization": "Bearer zspat_..."}
)
member = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/team/2",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const member = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/team/2")
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/team/{id}
Update an admin user's name and role assignment.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Admin user ID |
{
"name": "Bob Jones Jr.",
"role_id": 2
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/team/2" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"name": "Bob Jones Jr.", "role_id": 2}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/team/2",
headers={"Authorization": "Bearer zspat_..."},
json={"name": "Bob Jones Jr.", "role_id": 2}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/team/2",
{
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "Bob Jones Jr.", role_id: 2 })
}
);
// 204 No Content
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/team/2")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { name: "Bob Jones Jr.", role_id: 2 }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# 204 No Content
/admin/api/team/{id}
Remove an admin user. You cannot delete your own account.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Admin user ID |
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/team/3" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/team/3",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/team/3",
{
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
}
);
// 204 No Content
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/team/3")
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/roles
List all roles with their assigned permissions.
{
"roles": [
{
"id": 1,
"name": "Editor",
"permissions": [
"products.view",
"products.create",
"products.edit",
"orders.view"
],
"created_at": "2025-10-15T09:00:00Z"
},
{
"id": 2,
"name": "Viewer",
"permissions": [
"dashboard.view",
"products.view",
"orders.view"
],
"created_at": "2025-10-15T09:05:00Z"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/roles" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/roles",
headers={"Authorization": "Bearer zspat_..."}
)
roles = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/roles",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/roles")
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/roles
Create a new role with a set of permissions.
{
"name": "Editor",
"permissions": [
"products.view",
"products.create",
"products.edit",
"orders.view"
]
}
{
"id": 3,
"name": "Editor",
"permissions": [
"products.view",
"products.create",
"products.edit",
"orders.view"
],
"created_at": ""
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/roles" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"name": "Editor", "permissions": ["products.view", "products.create", "products.edit", "orders.view"]}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/roles",
headers={"Authorization": "Bearer zspat_..."},
json={
"name": "Editor",
"permissions": ["products.view", "products.create", "products.edit", "orders.view"]
}
)
role = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/roles",
{
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Editor",
permissions: ["products.view", "products.create", "products.edit", "orders.view"]
})
}
);
const role = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/roles")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { name: "Editor", permissions: ["products.view", "products.create"] }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/roles/{id}
Get a single role with its permissions.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Role ID |
{
"id": 1,
"name": "Editor",
"permissions": [
"products.view",
"products.create",
"products.edit",
"orders.view"
],
"created_at": "2025-10-15T09:00:00Z"
}
curl "https://yourshop.zeroshop.io/admin/api/roles/1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/roles/1",
headers={"Authorization": "Bearer zspat_..."}
)
role = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/roles/1",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const role = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/roles/1")
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/roles/{id}
Update a role's name and permissions. Replaces the entire permission set.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Role ID |
{
"name": "Senior Editor",
"permissions": [
"products.view",
"products.create",
"products.edit",
"products.delete",
"orders.view",
"orders.edit"
]
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/roles/1" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"name": "Senior Editor", "permissions": ["products.view", "products.create", "products.edit", "products.delete"]}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/roles/1",
headers={"Authorization": "Bearer zspat_..."},
json={"name": "Senior Editor", "permissions": ["products.view", "products.create"]}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/roles/1",
{
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "Senior Editor", permissions: ["products.view"] })
}
);
// 204 No Content
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/roles/1")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.content_type = "application/json"
req.body = { name: "Senior Editor", permissions: ["products.view"] }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
# 204 No Content
/admin/api/roles/{id}
Delete a role. Users assigned this role are promoted to Owner (role_id set to NULL).
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Role ID |
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/roles/1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/roles/1",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/roles/1",
{
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
}
);
// 204 No Content
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/roles/1")
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/personal-access-tokens
List the caller's personal access tokens. Filtered to the authenticated user -- you can never see another user's tokens.
{
"tokens": [
{
"id": 1,
"name": "CI pipeline",
"prefix": "a1b2c3d4e5f6",
"created_at": "2025-11-01T10:00:00Z",
"last_used_at": "2025-11-20T14:30:00Z",
"expires_at": "2026-01-29T10:00:00Z",
"revoked_at": null
},
{
"id": 2,
"name": "Local dev",
"prefix": "g7h8i9j0k1l2",
"created_at": "2025-11-10T08:00:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/personal-access-tokens" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/personal-access-tokens",
headers={"Authorization": "Bearer zspat_..."}
)
tokens = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/personal-access-tokens",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/personal-access-tokens")
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/personal-access-tokens
Create a new personal access token. Cookie-only -- PAT-authenticated callers receive 403. The plaintext token is returned exactly once. Max 20 active tokens per user.
{
"name": "CI pipeline",
"expires_in_days": 90
}
{
"id": 3,
"name": "CI pipeline",
"token": "zspat_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w",
"prefix": "a1b2c3d4e5f6",
"expires_at": "2026-02-18T10:00:00Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/personal-access-tokens" \
-H "Cookie: zeroshop_admin_jwt=..." \
-H "Content-Type: application/json" \
-d '{"name": "CI pipeline", "expires_in_days": 90}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/personal-access-tokens",
cookies={"zeroshop_admin_jwt": "..."},
json={"name": "CI pipeline", "expires_in_days": 90}
)
data = resp.json()
print(f"Save this token: {data['token']}") # shown once!
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/personal-access-tokens",
{
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "CI pipeline", expires_in_days: 90 })
}
);
const data = await resp.json();
console.log("Save this token:", data.token); // shown once!
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/personal-access-tokens")
req = Net::HTTP::Post.new(uri)
req["Cookie"] = "zeroshop_admin_jwt=..."
req.content_type = "application/json"
req.body = { name: "CI pipeline", expires_in_days: 90 }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
puts "Save this token: #{data['token']}" # shown once!
/admin/api/personal-access-tokens/{id}
Revoke a personal access token. You can only revoke your own tokens -- attempts to revoke another user's token return 404.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Token ID |
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/personal-access-tokens/1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/personal-access-tokens/1",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/personal-access-tokens/1",
{
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
}
);
// 204 No Content
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/personal-access-tokens/1")
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
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.