Notifications, logs, dashboard metrics, and per-user email prefs
/admin/api/notifications
List notifications with unread count. All authenticated admins can see notifications (no permission check).
/admin/api/notifications/{id}/read
Mark a single notification as read.
/admin/api/notifications/read-all
Mark all notifications as read. Returns the count of notifications that were marked.
/admin/api/logs
List admin log entries (webhook events, MCP tool invocations, etc.) with optional source filter and pagination.
/admin/api/dashboard
Returns key shop metrics: totals, recent orders, low stock alerts, and operational status counts.
/admin/api/dashboard/trends
Returns time-series trend data (daily revenue and order counts) plus a today-vs-yesterday comparison.
/admin/api/dashboard/insights
Returns business insights: top products by revenue, top customers by lifetime value, repeat customer rate, MRR, and recently cancelled subscriptions.
/admin/api/subscriptions
List subscriptions with pagination. Optionally filter by status, customer, or product.
/admin/api/subscriptions/{id}
Get full details of a single subscription including the linked initial order.
/admin/api/subscriptions/{id}/invoices
List all invoices for a subscription, newest first.
/admin/api/subscriptions/{id}/cancel
Force-cancel a subscription immediately (admin override). If Stripe credentials are configured, the subscription is also cancelled on Stripe.
/admin/api/dashboard/subscriptions
Returns at-a-glance counts of subscriptions grouped by status, plus the grand total.
/admin/api/me/notification-preferences
List the calling user's notification preferences for every event type they have permission to see. Default-on for missing rows is applied here so callers do not need to know about the absence-as-subscribed convention.
/admin/api/me/notification-preferences/{event_type}
Set the calling user's email-enabled flag for a single event type. Returns 404 for unknown event types and 403 if the caller lacks the gating permission for that event.
/admin/api/notifications
List notifications with unread count. All authenticated admins can see notifications (no permission check).
| Name | Type | Required | Description |
|---|---|---|---|
| limit | integer | optional | Maximum notifications to return, 1-100 (default: 20) |
| offset | integer | optional | Offset for pagination (default: 0) |
{
"unread_count": 3,
"notifications": [
{
"id": 5,
"type": "smtp_error",
"severity": "error",
"title": "SMTP delivery failed",
"message": "Order confirmation email to customer@example.com could not be delivered.",
"metadata": {
"order_id": 42
},
"read": false,
"created_at": "2025-11-20T14:30:00Z"
},
{
"id": 4,
"type": "low_stock",
"severity": "warning",
"title": "Low stock alert",
"message": "Classic Cotton Tee has only 3 units remaining.",
"metadata": {
"product_id": 1,
"stock": 3
},
"read": false,
"created_at": "2025-11-20T10:00:00Z"
},
{
"id": 3,
"type": "personal_access_token_created",
"severity": "info",
"title": "Personal access token created",
"message": "Token \"CI pipeline\" was created.",
"metadata": null,
"read": true,
"created_at": "2025-11-19T08:00:00Z"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/notifications?limit=20&offset=0" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/notifications",
params={"limit": 20, "offset": 0},
headers={"Authorization": "Bearer zspat_..."}
)
data = resp.json()
print(f"Unread: {data['unread_count']}")
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/notifications?limit=20&offset=0",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const data = await resp.json();
console.log("Unread:", data.unread_count);
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/notifications?limit=20&offset=0")
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/notifications/{id}/read
Mark a single notification as read.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Notification ID |
curl -X PATCH "https://yourshop.zeroshop.io/admin/api/notifications/5/read" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.patch(
"https://yourshop.zeroshop.io/admin/api/notifications/5/read",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/notifications/5/read",
{
method: "PATCH",
headers: { "Authorization": "Bearer zspat_..." }
}
);
// 204 No Content
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/notifications/5/read")
req = Net::HTTP::Patch.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/notifications/read-all
Mark all notifications as read. Returns the count of notifications that were marked.
{
"marked": 3
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/notifications/read-all" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/notifications/read-all",
headers={"Authorization": "Bearer zspat_..."}
)
data = resp.json()
print(f"Marked {data['marked']} notifications as read")
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/notifications/read-all",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
}
);
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/notifications/read-all")
req = Net::HTTP::Post.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/logs
List admin log entries (webhook events, MCP tool invocations, etc.) with optional source filter and pagination.
| Name | Type | Required | Description |
|---|---|---|---|
| source | string | optional | Filter by source (e.g. "webhook", "mcp"). Omit for all sources. |
| page | integer | optional | Page number, 1-indexed (default: 1) |
{
"logs": [
{
"id": 100,
"source": "webhook",
"action": "checkout.session.completed",
"status": "success",
"summary": "Processed Stripe checkout for order #ORD-0042",
"detail": null,
"created_at": "2025-11-20T14:30:00Z"
},
{
"id": 99,
"source": "mcp",
"action": "create_product",
"status": "success",
"summary": "Created product \"Classic Cotton Tee\"",
"detail": null,
"created_at": "2025-11-20T14:25:00Z"
}
],
"total": 250,
"page": 1,
"total_pages": 13
}
curl "https://yourshop.zeroshop.io/admin/api/logs?source=webhook&page=1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/logs",
params={"source": "webhook", "page": 1},
headers={"Authorization": "Bearer zspat_..."}
)
data = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/logs?source=webhook&page=1",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/logs?source=webhook&page=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/dashboard
Returns key shop metrics: totals, recent orders, low stock alerts, and operational status counts.
{
"total_orders": 142,
"total_revenue": 1450000,
"total_products": 35,
"recent_orders": [
{
"id": 142,
"status": "pending",
"customer_name": "Jane Doe",
"total": 5998,
"created_at": "2025-11-20T14:30:00Z"
},
{
"id": 141,
"status": "shipped",
"customer_name": "Bob Smith",
"total": 2999,
"created_at": "2025-11-20T10:15:00Z"
}
],
"low_stock_products": [
{
"id": 5,
"name": "Limited Edition Hoodie",
"stock": 2
},
{
"id": 12,
"name": "Vintage T-Shirt",
"stock": 4
}
],
"orders_by_status": {
"pending": 12,
"processing": 5,
"shipped": 98,
"delivered": 25,
"cancelled": 2
},
"orders_needing_attention": 8,
"failed_payment_orders": 1,
"out_of_stock_count": 3,
"unanswered_inquiries": 4,
"total_customers": 89,
"new_customers_this_month": 12
}
curl "https://yourshop.zeroshop.io/admin/api/dashboard" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/dashboard",
headers={"Authorization": "Bearer zspat_..."}
)
dashboard = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/dashboard",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const dashboard = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/dashboard")
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/dashboard/trends
Returns time-series trend data (daily revenue and order counts) plus a today-vs-yesterday comparison.
| Name | Type | Required | Description |
|---|---|---|---|
| days | integer | optional | Number of days to look back, 1-90 (default: 30) |
{
"daily": [
{
"date": "2025-11-18",
"revenue": 45000,
"orders": 5
},
{
"date": "2025-11-19",
"revenue": 32000,
"orders": 3
},
{
"date": "2025-11-20",
"revenue": 59980,
"orders": 7
}
],
"today": {
"revenue": 59980,
"orders": 7
},
"yesterday": {
"revenue": 32000,
"orders": 3
}
}
curl "https://yourshop.zeroshop.io/admin/api/dashboard/trends?days=30" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/dashboard/trends",
params={"days": 30},
headers={"Authorization": "Bearer zspat_..."}
)
trends = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/dashboard/trends?days=30",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const trends = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/dashboard/trends?days=30")
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/dashboard/insights
Returns business insights: top products by revenue, top customers by lifetime value, repeat customer rate, MRR, and recently cancelled subscriptions.
{
"top_products": [
{
"product_id": 1,
"name": "Classic Cotton Tee",
"revenue": 89970,
"quantity": 30
},
{
"product_id": 3,
"name": "Premium Hoodie",
"revenue": 74850,
"quantity": 15
}
],
"top_customers": [
{
"customer_id": 12,
"name": "Jane Doe",
"email": "jane@example.com",
"total_spent": 45000,
"order_count": 5
},
{
"customer_id": 8,
"name": "Bob Smith",
"email": "bob@example.com",
"total_spent": 32000,
"order_count": 3
}
],
"repeat_customer_rate": 0.34,
"mrr": 99900,
"recently_canceled": [
{
"id": 5,
"public_id": "SUB-0005",
"product_name": "Monthly Coffee Box",
"canceled_at": "2025-11-18T09:00:00Z"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/dashboard/insights" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/dashboard/insights",
headers={"Authorization": "Bearer zspat_..."}
)
insights = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/dashboard/insights",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const insights = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/dashboard/insights")
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/subscriptions
List subscriptions with pagination. Optionally filter by status, customer, or product.
| Name | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number, 1-indexed (default: 1) |
| per_page | integer | optional | Items per page, 1-100 (default: 20) |
| status | string | optional | Filter by lifecycle status: "active", "past_due", "canceled", "incomplete", "incomplete_expired", "trialing", "unpaid" |
| customer_id | integer | optional | Filter by local customer ID |
| product_id | integer | optional | Filter by local product ID |
{
"data": [
{
"id": 1,
"public_id": "SUB-0001",
"status": "active",
"product_name": "Monthly Coffee Box",
"variant_label": "Standard",
"customer_id": 12,
"customer_name": "Jane Doe",
"customer_email": "jane@example.com",
"provider": "stripe",
"external_subscription_id": "sub_1N7abc",
"billing_interval": "month",
"billing_interval_count": 1,
"cancellation_policy": "end_of_period",
"max_billing_cycles": null,
"unit_amount": "$19.99",
"unit_amount_cents": 1999,
"currency": "USD",
"quantity": 1,
"current_period_start": "2025-11-01T00:00:00Z",
"current_period_end": "2025-12-01T00:00:00Z",
"cancel_at_period_end": false,
"canceled_at": null,
"created_at": "2025-10-01T10:00:00Z"
}
],
"page": 1,
"per_page": 20,
"total": 15,
"total_pages": 1
}
curl "https://yourshop.zeroshop.io/admin/api/subscriptions?page=1&status=active" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/subscriptions",
params={"page": 1, "status": "active"},
headers={"Authorization": "Bearer zspat_..."}
)
subs = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/subscriptions?page=1&status=active",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const subs = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/subscriptions?page=1&status=active")
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/subscriptions/{id}
Get full details of a single subscription including the linked initial order.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Subscription ID |
{
"id": 1,
"public_id": "SUB-0001",
"status": "active",
"product_name": "Monthly Coffee Box",
"variant_label": "Standard",
"customer_id": 12,
"customer_name": "Jane Doe",
"customer_email": "jane@example.com",
"provider": "stripe",
"external_subscription_id": "sub_1N7abc",
"billing_interval": "month",
"billing_interval_count": 1,
"cancellation_policy": "end_of_period",
"max_billing_cycles": null,
"unit_amount": "$19.99",
"unit_amount_cents": 1999,
"currency": "USD",
"quantity": 1,
"current_period_start": "2025-11-01T00:00:00Z",
"current_period_end": "2025-12-01T00:00:00Z",
"cancel_at_period_end": false,
"canceled_at": null,
"ended_at": null,
"initial_order_id": 42,
"initial_order_public_id": "ORD-0042",
"created_at": "2025-10-01T10:00:00Z",
"updated_at": "2025-11-01T00:00:00Z"
}
curl "https://yourshop.zeroshop.io/admin/api/subscriptions/1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/subscriptions/1",
headers={"Authorization": "Bearer zspat_..."}
)
sub = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/subscriptions/1",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const sub = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/subscriptions/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/subscriptions/{id}/invoices
List all invoices for a subscription, newest first.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Subscription ID |
[
{
"id": 3,
"external_invoice_id": "in_1N7xyz",
"external_payment_intent": "pi_1N7xyz",
"amount_total": "$19.99",
"amount_total_cents": 1999,
"amount_paid": "$19.99",
"amount_paid_cents": 1999,
"currency": "USD",
"status": "paid",
"period_start": "2025-11-01T00:00:00Z",
"period_end": "2025-12-01T00:00:00Z",
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_1N7abc/inv_abc123",
"created_at": "2025-11-01T00:05:00Z"
},
{
"id": 2,
"external_invoice_id": "in_1N6xyz",
"external_payment_intent": "pi_1N6xyz",
"amount_total": "$19.99",
"amount_total_cents": 1999,
"amount_paid": "$19.99",
"amount_paid_cents": 1999,
"currency": "USD",
"status": "paid",
"period_start": "2025-10-01T00:00:00Z",
"period_end": "2025-11-01T00:00:00Z",
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_1N7abc/inv_def456",
"created_at": "2025-10-01T00:05:00Z"
}
]
curl "https://yourshop.zeroshop.io/admin/api/subscriptions/1/invoices" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/subscriptions/1/invoices",
headers={"Authorization": "Bearer zspat_..."}
)
invoices = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/subscriptions/1/invoices",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const invoices = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/subscriptions/1/invoices")
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/subscriptions/{id}/cancel
Force-cancel a subscription immediately (admin override). If Stripe credentials are configured, the subscription is also cancelled on Stripe.
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | required | Subscription ID |
curl -X POST "https://yourshop.zeroshop.io/admin/api/subscriptions/1/cancel" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/subscriptions/1/cancel",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/subscriptions/1/cancel",
{
method: "POST",
headers: { "Authorization": "Bearer zspat_..." }
}
);
// 204 No Content
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/subscriptions/1/cancel")
req = Net::HTTP::Post.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/dashboard/subscriptions
Returns at-a-glance counts of subscriptions grouped by status, plus the grand total.
{
"total": 15,
"by_status": {
"active": 10,
"past_due": 2,
"canceled": 3
}
}
curl "https://yourshop.zeroshop.io/admin/api/dashboard/subscriptions" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/dashboard/subscriptions",
headers={"Authorization": "Bearer zspat_..."}
)
summary = resp.json()
const resp = await fetch(
"https://yourshop.zeroshop.io/admin/api/dashboard/subscriptions",
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const summary = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/dashboard/subscriptions")
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/me/notification-preferences
List the calling user's notification preferences for every event type they have permission to see. Default-on for missing rows is applied here so callers do not need to know about the absence-as-subscribed convention.
[
{
"event_type": "order_placed",
"label": "New order placed",
"email_enabled": true
},
{
"event_type": "inquiry_received",
"label": "Buyer inquiry received",
"email_enabled": false
}
]
curl "https://yourshop.zeroshop.io/admin/api/me/notification-preferences" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/me/notification-preferences",
headers={"Authorization": "Bearer zspat_..."}
)
prefs = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/me/notification-preferences", {
headers: { "Authorization": "Bearer zspat_..." }
});
const prefs = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/me/notification-preferences")
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) }
prefs = JSON.parse(resp.body)
/admin/api/me/notification-preferences/{event_type}
Set the calling user's email-enabled flag for a single event type. Returns 404 for unknown event types and 403 if the caller lacks the gating permission for that event.
{
"email_enabled": false
}
{
"email_enabled": false
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/me/notification-preferences/order_placed" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"email_enabled": false}'
import requests
requests.put(
"https://yourshop.zeroshop.io/admin/api/me/notification-preferences/order_placed",
headers={"Authorization": "Bearer zspat_..."},
json={"email_enabled": False}
)
await fetch("https://yourshop.zeroshop.io/admin/api/me/notification-preferences/order_placed", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ email_enabled: false })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/me/notification-preferences/order_placed")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { email_enabled: false }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
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.