Clients
Clients endpoints of the Zanfia Workspace API, with schemas and code samples.
Manage contacts and product access. Adding a client creates a contact only — access is granted per product. All routes require a Authorization: Bearer header — see Authentication.
List clients
/clientsAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Response Body
application/json
application/json
curl -X GET "https://example.com/clients"{ "items": [ { "id": "string", "email": "string", "firstName": "string", "lastName": "string", "phoneNumber": "string", "totalOrders": 0, "totalSpentCents": 0, "activeProducts": 0, "language": "string", "discordId": "string", "githubId": 0, "tagIds": [ "string" ], "emailSubscriptionStatus": "pending", "attribution": { "utmSource": "string", "utmMedium": "string", "utmCampaign": "string", "utmTerm": "string", "utmContent": "string", "fbclid": "string", "gclid": "string", "metaCampaignId": "string", "metaAdId": "string", "metaCampaignName": "string", "metaAdName": "string", "capturedAt": "string" }, "createdAt": "string" } ], "nextCursor": "string"}{ "error": "string", "message": "string"}Add a client (contact only)
/clientsAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Create a CONTACT-ONLY client — a workspace customer with no product grant and no order. Granting product access is a separate action (not exposed here yet).
Response Body
application/json
application/json
curl -X POST "https://example.com/clients" \ -H "Content-Type: application/json" \ -d '{ "email": "string" }'{ "clientId": "string", "created": true, "createdUser": true}{ "error": "string", "message": "string"}List client tags
/clients/tagsAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Response Body
application/json
application/json
curl -X GET "https://example.com/clients/tags"{ "count": 0, "tags": [ { "id": "string", "name": "string", "color": "string" } ]}{ "error": "string", "message": "string"}Export clients as CSV
/clients/exportAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Query Parameters
Comma-separated optional column groups, any subset of: tags, stats, emailSubscription, activity, accesses, invoiceData, attribution, identifiers. Omit for identity columns only.
Case-insensitive prefix match on email / first name / last name.
has = only clients with active product access, no = only clients without any.
Only clients added on/after this ISO date (inclusive).
Only clients added on/before this ISO date (inclusive).
Row cap (default and max 20000).
Response Body
application/json
application/json
curl -X GET "https://example.com/clients/export"{ "csv": "string", "rowCount": 0, "truncated": true}{ "error": "string", "message": "string"}Revoke access for every client matching a filter
/clients/bulk-filtered-actionAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
POST /clients/bulk-filtered-action — revoke (archive) access in up to 10 products for EVERY client matching the Customers-list filter; the run resolves the client set server-side.
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/bulk-filtered-action" \ -H "Content-Type: application/json" \ -d '{ "action": "archive", "productIds": [ "string" ], "clientsFilter": {} }'{ "runId": "string"}{ "error": "string", "message": "string"}Delete clients
/clients/deleteAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
POST /clients/delete — up to 200 client ids per call.
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/delete" \ -H "Content-Type: application/json" \ -d '{ "clientIds": [ "string" ] }'{ "deletedClientIds": [ "string" ], "skipped": [ { "clientId": "string", "reason": "not-found" } ]}{ "error": "string", "message": "string"}Client detail
/clients/{clientId}Authorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Response Body
application/json
application/json
curl -X GET "https://example.com/clients/string"{ "id": "string", "email": "string", "firstName": "string", "lastName": "string", "phoneNumber": "string", "totalOrders": 0, "totalSpentCents": 0, "activeProducts": 0, "language": "string", "discordId": "string", "githubId": 0, "tagIds": [ "string" ], "emailSubscriptionStatus": "pending", "attribution": { "utmSource": "string", "utmMedium": "string", "utmCampaign": "string", "utmTerm": "string", "utmContent": "string", "fbclid": "string", "gclid": "string", "metaCampaignId": "string", "metaAdId": "string", "metaCampaignName": "string", "metaAdName": "string", "capturedAt": "string" }, "createdAt": "string", "previousEmails": [ "string" ], "firstSeen": "string", "lastSeen": "string", "invoiceData": { "companyName": "string", "nip": "string", "street": "string", "postalCode": "string", "city": "string", "country": "string" }, "invoiceProfiles": [ { "companyName": "string", "nip": "string", "street": "string", "postalCode": "string", "city": "string", "country": "string" } ]}{ "error": "string", "message": "string"}Update a client
/clients/{clientId}Authorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Partial update of a client's profile fields. Omitted fields are left unchanged.
Response Body
application/json
application/json
curl -X PATCH "https://example.com/clients/string" \ -H "Content-Type: application/json" \ -d '{}'{ "clientId": "string"}{ "error": "string", "message": "string"}List a client's product accesses
/clients/{clientId}/product-accessesAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Query Parameters
Filter on the derived access status. Omit for the complete list (all statuses).
Value in
- "active"
- "revoked"
- "expired"
Response Body
application/json
application/json
curl -X GET "https://example.com/clients/string/product-accesses"{ "items": [ { "accessId": "string", "productId": "string", "productName": "string", "productType": "course", "status": "active", "planId": "string", "paymentType": "one_time", "grantedAt": "string", "updatedAt": "string", "accessEndsAt": "string", "cancelAtPeriodEnd": true, "revokedAt": "string", "revokeReason": "access_end", "source": "purchase", "orderId": "string", "checkoutId": "string", "isTest": true } ]}{ "error": "string", "message": "string"}Bulk action on a product's clients
/products/{productId}/clients/bulk-actionAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Product id
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
POST /products/{productId}/clients/bulk-action — the product comes from the path. data is ArchiveClientsData for archive / delete and ChangeClientPeriodData for change_client_period.
Response Body
application/json
application/json
curl -X POST "https://example.com/products/string/clients/bulk-action" \ -H "Content-Type: application/json" \ -d '{ "action": "archive", "data": { "processAllClients": true, "runTasks": true } }'{ "runId": "string"}{ "error": "string", "message": "string"}List a product's clients
/products/{productId}/clientsAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Product id
Query Parameters
Filter on the derived access status; all returns every grant. Default: active.
Value in
- "active"
- "revoked"
- "expired"
- "all"
Legacy: stored grant status (archived = revoked + expired). Ignored when accessStatus is sent.
Value in
- "active"
- "archived"
Page size, max 100 (default 50).
The previous page's nextCursor.
Response Body
application/json
application/json
curl -X GET "https://example.com/products/string/clients"{ "items": [ { "accessId": "string", "productId": "string", "productName": "string", "productType": "course", "status": "active", "planId": "string", "paymentType": "one_time", "grantedAt": "string", "updatedAt": "string", "accessEndsAt": "string", "cancelAtPeriodEnd": true, "revokedAt": "string", "revokeReason": "access_end", "source": "purchase", "orderId": "string", "checkoutId": "string", "isTest": true, "clientId": "string", "email": "string", "firstName": "string", "lastName": "string", "phoneNumber": "string", "discordId": "string", "priceId": "string", "accessStartsAt": "string" } ], "nextCursor": "string"}{ "error": "string", "message": "string"}Grant product access
/products/{productId}/clientsAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Product id
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Grant a customer access to a product without a purchase (the dashboard's "New client access"). Default = free lifetime access; accessUntil makes a time-limited comp (free grants only); priceId enrolls under an existing price without billing. The welcome/sign-in email is sent only when sendWelcomeEmail is true.
Response Body
application/json
application/json
curl -X POST "https://example.com/products/string/clients" \ -H "Content-Type: application/json" \ -d '{ "email": "string" }'{ "granted": true, "isFree": true}{ "error": "string", "message": "string"}List a product's plan-bundled access holders
/products/{productId}/clients/bundled-accessAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Product id
Response Body
application/json
application/json
curl -X GET "https://example.com/products/string/clients/bundled-access"{ "items": [ { "clientId": "string", "email": "string", "firstName": "string", "lastName": "string", "viaProductId": "string", "viaProductName": "string", "viaPlanName": "string", "sinceAt": "string" } ]}{ "error": "string", "message": "string"}Revoke product access
/products/{productId}/clients/revoke-accessAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Product id
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Revoke a customer's product access. NO refund is issued and the payment gateway is untouched — customers with a live subscription are refused (409); cancel the subscription first. Reversible via the grant route.
Response Body
application/json
application/json
curl -X POST "https://example.com/products/string/clients/revoke-access" \ -H "Content-Type: application/json" \ -d '{ "email": "string" }'{ "revoked": true}{ "error": "string", "message": "string"}Change an access period
/products/{productId}/clients/access-periodAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Product id
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Re-window a TIME-LIMITED (one-time) access period. Provide exactly one field.
Response Body
application/json
application/json
curl -X POST "https://example.com/products/string/clients/access-period" \ -H "Content-Type: application/json" \ -d '{ "email": "string" }'{ "accessEndsAt": "string"}{ "error": "string", "message": "string"}Cancel a client's subscription
/products/{productId}/clients/cancel-subscriptionAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Product id
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Creator-side cancel of ONE client's live subscription to ONE product (POST /products/:productId/clients/cancel-subscription). Stops billing; access runs to the end of the paid period (or ends immediately when the latest invoice is unpaid). Idempotent: a subscription already set to cancel at period end answers 200 again.
Response Body
application/json
application/json
curl -X POST "https://example.com/products/string/clients/cancel-subscription" \ -H "Content-Type: application/json" \ -d '{ "email": "string" }'{ "cancelled": true}{ "error": "string", "message": "string"}List client notes
/clients/{clientId}/notesAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Response Body
application/json
application/json
curl -X GET "https://example.com/clients/string/notes"{ "notes": [ { "id": "string", "text": "string", "authorId": "string", "authorName": "string", "createdAt": "string" } ]}{ "error": "string", "message": "string"}Add a client note
/clients/{clientId}/notesAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/string/notes" \ -H "Content-Type: application/json" \ -d '{ "text": "string" }'{ "note": { "id": "string", "text": "string", "authorId": "string", "authorName": "string", "createdAt": "string" }}{ "error": "string", "message": "string"}Client email delivery log
/clients/{clientId}/email-logAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Query Parameters
Newest entries to return (default 20, max 50).
Response Body
application/json
application/json
curl -X GET "https://example.com/clients/string/email-log"{ "email": "string", "entries": [ { "sentAt": "string", "statusUpdatedAt": "string", "subject": "string", "template": "string", "status": "submitted", "statusDetails": "string", "orderId": "string" } ]}{ "error": "string", "message": "string"}Change a client's email
/clients/{clientId}/change-emailAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/string/change-email" \ -H "Content-Type: application/json" \ -d '{ "newEmail": "string" }'{ "clientId": "string", "grantsUpdated": 0}{ "error": "string", "message": "string"}List a client's devices
/clients/{clientId}/devicesAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Response Body
application/json
application/json
curl -X GET "https://example.com/clients/string/devices"{ "devices": [ { "deviceId": "string", "clientKind": "web", "deviceName": "string", "createdAt": "string", "lastSeen": "string", "activeUntil": "string", "isCurrent": true } ]}{ "error": "string", "message": "string"}Revoke a client device
/clients/{clientId}/devices/{deviceId}/revokeAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Customer device slot id
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/string/devices/string/revoke"{ "status": "revoked", "deviceId": "string"}{ "error": "string", "message": "string"}Reset a client's devices
/clients/{clientId}/devices/resetAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Client id
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/string/devices/reset"{ "revokedCount": 0}{ "error": "string", "message": "string"}Bulk-import contacts
/clients/importAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Bulk contact import — one bounded batch of contact-only client creates (no product grant, no order), the row shape a CSV maps onto naturally. Each row goes through the same create path as POST /clients (real Auth identity, dedup by email); tagIds are applied to every resolved row. Larger files are chunked by the caller (max 100 rows per request).
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/import" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ { "email": "string" } ] }'{ "results": [ { "email": "string", "status": "created", "clientId": "string" } ], "created": 0, "existing": 0, "invalid": 0}{ "error": "string", "message": "string"}Preview a client merge
/clients/merge-previewAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Response Body
application/json
application/json
curl -X GET "https://example.com/clients/merge-preview"{ "preview": { "workspaceId": "string", "survivorWcid": "string", "loserWcid": "string", "survivorEmail": "string", "loserEmail": "string", "resultingPreviousEmails": [ "string" ], "money": { "survivorTotalSpentMinor": 0, "loserTotalSpentMinor": 0, "sumMinor": 0, "note": "string", "ordersMovingCount": 0 }, "grants": [ { "productId": "string", "productName": "string", "action": "move", "reason": "string" } ], "blocked": { "reason": "active-subscription", "subscriptions": [ { "productId": "string", "subscriptionId": "string", "side": "survivor" } ] }, "profileOverwrites": [ { "field": "firstName", "survivorValueKept": "string", "loserValueDiscarded": "string", "downstreamUnlink": true } ], "invoiceProfiles": { "survivorCount": 0, "loserCount": 0, "note": "string" }, "community": { "computed": true, "communities": [ { "communityId": "string", "communityName": "string", "action": "merge", "authoredRePointCount": 0 } ], "communitiesTouchedCount": 0, "authoredRePointCount": 0, "note": "string" }, "irreversible": true, "previewHash": "string" }}{ "error": "string", "message": "string"}Merge duplicate clients
/clients/mergeAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
⚠️ DESTRUCTIVE AND IRREVERSIBLE: merges the duplicate client into the survivor — grants move/dedupe, community membership and authored content re-point, profiles coalesce, and the duplicate client is TOMBSTONED. There is no undo route (recovery is an operator-level restore from the merge snapshot). Requires a fresh previewHash from GET /clients/merge-preview; execute aborts with 409 if the recomputed plan differs. A merge never touches Stripe — a live subscription on either side blocks it (409).
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/merge" \ -H "Content-Type: application/json" \ -d '{ "survivorClientId": "string", "duplicateClientId": "string", "previewHash": "string" }'{ "survivorClientId": "string", "grantsProcessed": 0, "merged": true}{ "error": "string", "message": "string"}Was this article helpful?

