View customer profiles and order history
/admin/api/customers
List customers with pagination, filtering, sorting, and search.
/admin/api/customers/{id}
Fetch a single customer by ID, including saved addresses and order count.
/admin/api/customers/{id}/files
List a customer's personal files, newest first. Returns 404 if the customer doesn't exist.
/admin/api/customers/{id}/files
Upload a personal file for this customer (multipart). Tier-gated by per-file and aggregate digital storage caps; returns 402 when caps are exceeded. Returns 404 if the customer doesn't exist, 422 when no file is provided.
/admin/api/customers/{id}/files/{file_id}
Delete a customer file: removes the row and best-effort deletes the underlying storage object. Returns 404 if the file doesn't exist or belongs to a different customer (existence is not disclosed).
/admin/api/customers/{id}/access-groups
List all access groups this customer currently belongs to, with one row per (group × source) so an admin can see exactly how each entitlement was earned. Sources are "order", "subscription", and "manual_grant". Returns 404 if the customer doesn't exist.
/admin/api/customers/{id}/b2b
Set a customer's B2B flag and optionally assign a discount group. Pass discount_group_id: null (or omit it) to clear the assignment. Returns 204 No Content on success.
/admin/api/customers/{id}/credits
Return a customer's live store-credit balance and the active (non-expired) buckets that make it up, sorted soonest-expiring first with non-expiring buckets last.
/admin/api/customers/{id}/credits/transactions
Return a paginated slice of the customer's credit ledger, newest first. Uses cursor pagination: pass before_id to fetch the next (older) page.
/admin/api/customers/{id}/credits/adjust
Apply a manual credit adjustment. A positive delta grants credit into a new bucket (optionally with an expiry); a negative delta debits existing buckets FIFO by soonest expiry. Debits are all-or-nothing: a debit larger than the available balance is rejected with 422 and nothing is changed. Returns the updated balance.
/admin/api/customers/{id}/notes
List all internal notes for a customer, newest first.
/admin/api/customers/{id}/notes
Create an internal note for a customer. The author is taken from the authenticated admin, not the request body. The body is trimmed and must be non-empty.
/admin/api/customers/{id}/notes/{note_id}
Delete a customer note by id. Returns 204 No Content on success, 404 if the note doesn't exist.
/admin/api/customers
List customers with pagination, filtering, sorting, and search.
| 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 customer name or email |
| registered | string | optional | Filter by registration status: "true" or "false" |
| created_from | string | optional | Earliest creation date (inclusive), as YYYY-MM-DD |
| created_to | string | optional | Latest creation date (inclusive), as YYYY-MM-DD |
| sort | string | optional | Sort field with optional - prefix for descending. Valid: name, email, created_at. Default: -created_at |
{
"data": [
{
"id": 1,
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+1-555-0123",
"registered": true,
"created_at": "2025-11-15T08:20:00Z"
},
{
"id": 2,
"name": "John Smith",
"email": "john@example.com",
"phone": null,
"registered": false,
"created_at": "2025-11-14T16:45:00Z"
}
],
"page": 1,
"per_page": 20,
"total": 156,
"total_pages": 8
}
curl "https://yourshop.zeroshop.io/admin/api/customers?page=1&per_page=20&search=jane&sort=-created_at" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/customers",
params={"page": 1, "per_page": 20, "search": "jane", "sort": "-created_at"},
headers={"Authorization": "Bearer zspat_..."}
)
customers = resp.json()
const params = new URLSearchParams({ page: 1, per_page: 20, search: "jane", sort: "-created_at" });
const resp = await fetch(
`https://yourshop.zeroshop.io/admin/api/customers?${params}`,
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers?page=1&per_page=20&search=jane")
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/customers/{id}
Fetch a single customer by ID, including saved addresses and order count.
{
"id": 1,
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+1-555-0123",
"registered": true,
"register_ip": "203.0.113.42",
"last_login_ip": "198.51.100.7",
"created_at": "2025-11-15T08:20:00Z",
"order_count": 5,
"addresses": [
{
"id": 1,
"address_type": "shipping",
"label": "Home",
"line1": "123 Main Street",
"line2": "Apt 4B",
"city": "Portland",
"state": "OR",
"postal_code": "97201",
"country": "US",
"is_default": true
},
{
"id": 2,
"address_type": "billing",
"label": "Work",
"line1": "456 Business Ave",
"line2": null,
"city": "Portland",
"state": "OR",
"postal_code": "97204",
"country": "US",
"is_default": false
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/customers/1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/customers/1",
headers={"Authorization": "Bearer zspat_..."}
)
customer = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1", {
headers: { "Authorization": "Bearer zspat_..." }
});
const customer = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/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) }
customer = JSON.parse(resp.body)
/admin/api/customers/{id}/files
List a customer's personal files, newest first. Returns 404 if the customer doesn't exist.
[
{
"id": 12,
"customer_id": 1,
"storage_key": "tenant-7/customer-files/1/2f8a1c4e-9b0d-4e23-8a11-1c2d3e4f5a6b.pdf",
"name": "coaching-plan-week-1.pdf",
"size_bytes": 245678,
"content_type": "application/pdf",
"note": "Week 1 strength block — review before Monday session.",
"created_at": "2026-04-22T10:15:00Z"
},
{
"id": 9,
"customer_id": 1,
"storage_key": "tenant-7/customer-files/1/a3b9e2f1-7c8d-4a55-b201-9e8f7d6c5b4a.zip",
"name": "intake-photos.zip",
"size_bytes": 8429310,
"content_type": "application/zip",
"note": null,
"created_at": "2026-04-05T08:00:00Z"
}
]
curl "https://yourshop.zeroshop.io/admin/api/customers/1/files" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/customers/1/files",
headers={"Authorization": "Bearer zspat_..."}
)
files = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/files", {
headers: { "Authorization": "Bearer zspat_..." }
});
const files = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/files")
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) }
files = JSON.parse(resp.body)
/admin/api/customers/{id}/files
Upload a personal file for this customer (multipart). Tier-gated by per-file and aggregate digital storage caps; returns 402 when caps are exceeded. Returns 404 if the customer doesn't exist, 422 when no file is provided.
{
"type": "multipart/form-data",
"fields": [
{
"name": "file",
"type": "file",
"required": true,
"description": "The binary file contents."
},
{
"name": "name",
"type": "string",
"required": false,
"description": "Display name override. Defaults to the uploaded filename. Trimmed; empty values fall back to the upload filename."
},
{
"name": "note",
"type": "string",
"required": false,
"description": "Merchant-visible note shown to the customer (max 1000 characters)."
}
]
}
{
"id": 13,
"customer_id": 1,
"storage_key": "tenant-7/customer-files/1/c4d5e6f7-1a2b-4c3d-8e9f-0a1b2c3d4e5f.pdf",
"name": "coaching-plan-week-2.pdf",
"size_bytes": 251204,
"content_type": "application/pdf",
"note": "Week 2 — increase squat load to 70%.",
"created_at": "2026-05-12T14:32:11Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/customers/1/files" \
-H "Authorization: Bearer zspat_..." \
-F "file=@./coaching-plan-week-2.pdf" \
-F "name=coaching-plan-week-2.pdf" \
-F "note=Week 2 — increase squat load to 70%."
import requests
with open("coaching-plan-week-2.pdf", "rb") as fh:
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/customers/1/files",
headers={"Authorization": "Bearer zspat_..."},
files={"file": ("coaching-plan-week-2.pdf", fh, "application/pdf")},
data={"name": "coaching-plan-week-2.pdf", "note": "Week 2 — increase squat load to 70%."}
)
created = resp.json()
const form = new FormData();
form.append("file", fileInput.files[0]);
form.append("name", "coaching-plan-week-2.pdf");
form.append("note", "Week 2 — increase squat load to 70%.");
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/files", {
method: "POST",
headers: { "Authorization": "Bearer zspat_..." },
body: form
});
const created = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/files")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req.set_form(
[
["file", File.open("coaching-plan-week-2.pdf"), { filename: "coaching-plan-week-2.pdf", content_type: "application/pdf" }],
["name", "coaching-plan-week-2.pdf"],
["note", "Week 2 — increase squat load to 70%."]
],
"multipart/form-data"
)
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
created = JSON.parse(resp.body)
/admin/api/customers/{id}/files/{file_id}
Delete a customer file: removes the row and best-effort deletes the underlying storage object. Returns 404 if the file doesn't exist or belongs to a different customer (existence is not disclosed).
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/customers/1/files/12" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/customers/1/files/12",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/files/12", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/files/12")
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) }
/admin/api/customers/{id}/access-groups
List all access groups this customer currently belongs to, with one row per (group × source) so an admin can see exactly how each entitlement was earned. Sources are "order", "subscription", and "manual_grant". Returns 404 if the customer doesn't exist.
[
{
"customer_id": 1,
"customer_email": "jane@example.com",
"customer_name": "Jane Doe",
"source": "manual_grant",
"source_id": 42,
"expires_at": "2026-12-31T23:59:59Z",
"note": "Comped lifetime access for beta feedback.",
"group_name": "VIP Coaching",
"group_slug": "vip-coaching"
},
{
"customer_id": 1,
"customer_email": "jane@example.com",
"customer_name": "Jane Doe",
"source": "order",
"source_id": 1043,
"expires_at": null,
"note": null,
"group_name": "Foundations Program",
"group_slug": "foundations-program"
},
{
"customer_id": 1,
"customer_email": "jane@example.com",
"customer_name": "Jane Doe",
"source": "subscription",
"source_id": 17,
"expires_at": null,
"note": null,
"group_name": "Monthly Members",
"group_slug": "monthly-members"
}
]
curl "https://yourshop.zeroshop.io/admin/api/customers/1/access-groups" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/customers/1/access-groups",
headers={"Authorization": "Bearer zspat_..."}
)
memberships = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/access-groups", {
headers: { "Authorization": "Bearer zspat_..." }
});
const memberships = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/access-groups")
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) }
memberships = JSON.parse(resp.body)
/admin/api/customers/{id}/b2b
Set a customer's B2B flag and optionally assign a discount group. Pass discount_group_id: null (or omit it) to clear the assignment. Returns 204 No Content on success.
{
"is_b2b": true,
"discount_group_id": 3
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/customers/1/b2b" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"is_b2b": true, "discount_group_id": 3}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/customers/1/b2b",
headers={"Authorization": "Bearer zspat_..."},
json={"is_b2b": True, "discount_group_id": 3}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/b2b", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ is_b2b: true, discount_group_id: 3 })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/b2b")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { is_b2b: true, discount_group_id: 3 }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/customers/{id}/credits
Return a customer's live store-credit balance and the active (non-expired) buckets that make it up, sorted soonest-expiring first with non-expiring buckets last.
{
"credits": 1500,
"buckets": [
{
"id": 88,
"remaining_credits": 500,
"expires_at": "2026-08-01T00:00:00Z"
},
{
"id": 71,
"remaining_credits": 1000,
"expires_at": null
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/customers/1/credits" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/customers/1/credits",
headers={"Authorization": "Bearer zspat_..."}
)
balance = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/credits", {
headers: { "Authorization": "Bearer zspat_..." }
});
const balance = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/credits")
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) }
balance = JSON.parse(resp.body)
/admin/api/customers/{id}/credits/transactions
Return a paginated slice of the customer's credit ledger, newest first. Uses cursor pagination: pass before_id to fetch the next (older) page.
| Name | Type | Required | Description |
|---|---|---|---|
| before_id | integer | optional | Cursor: return only rows with id < before_id. Omit to fetch the latest page. |
| limit | integer | optional | Page size, 1-200 (default: 50). Values are clamped server-side. |
{
"transactions": [
{
"id": 412,
"kind": "manual_grant",
"delta_credits": 500,
"credit_id": 88,
"reference": null,
"note": "Goodwill credit for late shipment.",
"created_at": "2026-05-01T09:30:00Z",
"created_by": "admin:7"
},
{
"id": 405,
"kind": "deduct",
"delta_credits": -250,
"credit_id": 71,
"reference": "order:1043",
"note": null,
"created_at": "2026-04-28T14:12:00Z",
"created_by": "system"
}
]
}
curl "https://yourshop.zeroshop.io/admin/api/customers/1/credits/transactions?limit=50" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/customers/1/credits/transactions",
params={"limit": 50},
headers={"Authorization": "Bearer zspat_..."}
)
history = resp.json()
const params = new URLSearchParams({ limit: 50 });
const resp = await fetch(
`https://yourshop.zeroshop.io/admin/api/customers/1/credits/transactions?${params}`,
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const history = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/credits/transactions?limit=50")
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) }
history = JSON.parse(resp.body)
/admin/api/customers/{id}/credits/adjust
Apply a manual credit adjustment. A positive delta grants credit into a new bucket (optionally with an expiry); a negative delta debits existing buckets FIFO by soonest expiry. Debits are all-or-nothing: a debit larger than the available balance is rejected with 422 and nothing is changed. Returns the updated balance.
{
"delta": 500,
"expires_in_days": 90,
"note": "Goodwill credit for late shipment."
}
{
"credits": 2000,
"buckets": [
{
"id": 89,
"remaining_credits": 500,
"expires_at": "2026-08-28T00:00:00Z"
},
{
"id": 71,
"remaining_credits": 1000,
"expires_at": null
},
{
"id": 88,
"remaining_credits": 500,
"expires_at": "2026-08-01T00:00:00Z"
}
]
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/customers/1/credits/adjust" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"delta": 500,
"expires_in_days": 90,
"note": "Goodwill credit for late shipment."
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/customers/1/credits/adjust",
headers={"Authorization": "Bearer zspat_..."},
json={
"delta": 500,
"expires_in_days": 90,
"note": "Goodwill credit for late shipment."
}
)
balance = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/credits/adjust", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
delta: 500,
expires_in_days: 90,
note: "Goodwill credit for late shipment."
})
});
const balance = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/credits/adjust")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
delta: 500,
expires_in_days: 90,
note: "Goodwill credit for late shipment."
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
balance = JSON.parse(resp.body)
/admin/api/customers/{id}/notes
List all internal notes for a customer, newest first.
[
{
"id": 31,
"customer_id": 1,
"author": "Sam Admin",
"body": "Called about a missing parcel; reshipped and added store credit.",
"created_at": "2026-05-01T09:35:00Z"
},
{
"id": 18,
"customer_id": 1,
"author": "support@example.com",
"body": "Prefers email over phone.",
"created_at": "2026-03-12T11:00:00Z"
}
]
curl "https://yourshop.zeroshop.io/admin/api/customers/1/notes" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/customers/1/notes",
headers={"Authorization": "Bearer zspat_..."}
)
notes = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/notes", {
headers: { "Authorization": "Bearer zspat_..." }
});
const notes = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/notes")
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) }
notes = JSON.parse(resp.body)
/admin/api/customers/{id}/notes
Create an internal note for a customer. The author is taken from the authenticated admin, not the request body. The body is trimmed and must be non-empty.
{
"body": "Called about a missing parcel; reshipped and added store credit."
}
{
"id": 32,
"customer_id": 1,
"author": "Sam Admin",
"body": "Called about a missing parcel; reshipped and added store credit.",
"created_at": "2026-05-30T16:05:00Z"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/customers/1/notes" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"body": "Called about a missing parcel; reshipped and added store credit."}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/customers/1/notes",
headers={"Authorization": "Bearer zspat_..."},
json={"body": "Called about a missing parcel; reshipped and added store credit."}
)
note = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/notes", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ body: "Called about a missing parcel; reshipped and added store credit." })
});
const note = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/notes")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { body: "Called about a missing parcel; reshipped and added store credit." }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
note = JSON.parse(resp.body)
/admin/api/customers/{id}/notes/{note_id}
Delete a customer note by id. Returns 204 No Content on success, 404 if the note doesn't exist.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/customers/1/notes/32" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/customers/1/notes/32",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/customers/1/notes/32", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/customers/1/notes/32")
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) }
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.