Customers

View customer profiles and order history

GET /admin/api/customers

List customers with pagination, filtering, sorting, and search.

GET /admin/api/customers/{id}

Fetch a single customer by ID, including saved addresses and order count.

GET /admin/api/customers/{id}/files

List a customer's personal files, newest first. Returns 404 if the customer doesn't exist.

POST /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.

DELETE /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).

GET /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.

PUT /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.

GET /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.

GET /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.

POST /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.

GET /admin/api/customers/{id}/notes

List all internal notes for a customer, newest first.

POST /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.

DELETE /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.

GET /admin/api/customers

List customers with pagination, filtering, sorting, and search.

Parameters

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

Response 200

{
  "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
}

Example

curl "https://yourshop.zeroshop.io/admin/api/customers?page=1&per_page=20&search=jane&sort=-created_at" \
  -H "Authorization: Bearer zspat_..."
GET /admin/api/customers/{id}

Fetch a single customer by ID, including saved addresses and order count.

Response 200

{
  "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
    }
  ]
}

Example

curl "https://yourshop.zeroshop.io/admin/api/customers/1" \
  -H "Authorization: Bearer zspat_..."
GET /admin/api/customers/{id}/files

List a customer's personal files, newest first. Returns 404 if the customer doesn't exist.

Response 200

[
  {
    "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"
  }
]

Example

curl "https://yourshop.zeroshop.io/admin/api/customers/1/files" \
  -H "Authorization: Bearer zspat_..."
POST /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.

Request Body

{
  "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)."
    }
  ]
}

Response 201

{
  "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"
}

Example

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%."
DELETE /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).

Response 204

Example

curl -X DELETE "https://yourshop.zeroshop.io/admin/api/customers/1/files/12" \
  -H "Authorization: Bearer zspat_..."
GET /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.

Response 200

[
  {
    "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"
  }
]

Example

curl "https://yourshop.zeroshop.io/admin/api/customers/1/access-groups" \
  -H "Authorization: Bearer zspat_..."
PUT /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.

Request Body

{
  "is_b2b": true,
  "discount_group_id": 3
}

Response 204

Example

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}'
GET /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.

Response 200

{
  "credits": 1500,
  "buckets": [
    {
      "id": 88,
      "remaining_credits": 500,
      "expires_at": "2026-08-01T00:00:00Z"
    },
    {
      "id": 71,
      "remaining_credits": 1000,
      "expires_at": null
    }
  ]
}

Example

curl "https://yourshop.zeroshop.io/admin/api/customers/1/credits" \
  -H "Authorization: Bearer zspat_..."
GET /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.

Parameters

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.

Response 200

{
  "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"
    }
  ]
}

Example

curl "https://yourshop.zeroshop.io/admin/api/customers/1/credits/transactions?limit=50" \
  -H "Authorization: Bearer zspat_..."
POST /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.

Request Body

{
  "delta": 500,
  "expires_in_days": 90,
  "note": "Goodwill credit for late shipment."
}

Response 200

{
  "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"
    }
  ]
}

Example

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."
  }'
GET /admin/api/customers/{id}/notes

List all internal notes for a customer, newest first.

Response 200

[
  {
    "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"
  }
]

Example

curl "https://yourshop.zeroshop.io/admin/api/customers/1/notes" \
  -H "Authorization: Bearer zspat_..."
POST /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.

Request Body

{
  "body": "Called about a missing parcel; reshipped and added store credit."
}

Response 201

{
  "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"
}

Example

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."}'
DELETE /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.

Response 204

Example

curl -X DELETE "https://yourshop.zeroshop.io/admin/api/customers/1/notes/32" \
  -H "Authorization: Bearer zspat_..."

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.