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. Marketing consent is separate for email and SMS and can be set in bulk (POST /clients/email-subscription, POST /clients/sms-subscription, up to 200 clients per call). Tags are managed under /clients/tags (create, rename/recolour, delete) and assigned in bulk with POST /clients/tags/assignments; a newly added tag fires the client.TagAdded workflow trigger. All routes require a Authorization: Bearer header — see Authentication.
List clients
/clientsAuthorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Query Parameters
"Date added" window start, inclusive: an ISO 8601 date-time, or YYYY-MM-DD (the start of that day, UTC). Same filter as the Customers list in the dashboard; also accepted by GET /clients/export.
"Date added" window end, inclusive: an ISO 8601 date-time, or YYYY-MM-DD (the whole day, UTC). Must not be before createdFrom; a malformed value answers 400.
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" }, "customFields": { "property1": "string", "property2": "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, "marketingConsent": "granted"}{ "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"}Create a client tag
/clients/tagsAuthorization
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.
name 1-50 chars (trimmed), unique per workspace case-insensitively.
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/tags" \ -H "Content-Type: application/json" \ -d '{ "name": "string" }'{ "ok": true, "tag": { "id": "string", "name": "string", "color": "string", "createdAt": "string" }}{ "error": "string", "message": "string"}List custom contact fields
/clients/custom-fieldsAuthorization
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/custom-fields"{ "count": 0, "fields": [ { "key": "string", "name": "string", "type": "text", "options": [ { "key": "string", "label": "string", "archived": true } ], "archived": true, "createdAt": "string" } ]}{ "error": "string", "message": "string"}Add or remove tags on clients
/clients/tags/assignmentsAuthorization
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.
Add / remove tag IDS on up to 200 clients; addTagIds must exist in the registry.
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/tags/assignments" \ -H "Content-Type: application/json" \ -d '{ "clientIds": [ "string" ] }'{ "ok": true, "results": [ { "clientId": "string", "tags": [ "string" ] } ], "skippedClientIds": [ "string" ]}{ "error": "string", "message": "string"}Rename or recolour a client tag
/clients/tags/{tagId}Authorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Rename and/or recolour — the tag id (and every assignment) is untouched.
Response Body
application/json
application/json
curl -X PATCH "https://example.com/clients/tags/string" \ -H "Content-Type: application/json" \ -d '{}'{ "ok": true, "tag": { "id": "string", "name": "string", "color": "string", "createdAt": "string" }}{ "error": "string", "message": "string"}Delete a client tag
/clients/tags/{tagId}Authorization
bearerAuth API key from Dashboard → Integrations → API, MCP and CLI.
In: header
Path Parameters
Response Body
application/json
application/json
curl -X DELETE "https://example.com/clients/tags/string"{ "ok": true, "clientsUntagged": 0}{ "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" }, "customFields": { "property1": "string", "property2": "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. Access backed by a subscription is revoked once the subscription is over in Stripe (checked live, KI#1448); a subscription that is still live is refused (409 with its status) unless cancelSubscription is set, which cancels it now and revokes in one step. 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, "subscription": "none"}{ "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; one Stripe already ended answers alreadyCancelled: true and the access record is brought in line with it.
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, "alreadyCancelled": 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", "campaignId": "string", "automationId": "string", "tracked": true, "openedAt": "string", "clickedAt": "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", "skippedCustomFields": [ "string" ] } ], "created": 0, "existing": 0, "invalid": 0, "overLimit": 0, "customFieldValuesSkipped": 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"}Bulk subscribe/unsubscribe clients (email)
/clients/email-subscriptionAuthorization
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.
Set the marketing-email subscription of MANY clients at once (the Customers-list "Subscribe" / "Unsubscribe" bulk actions). Source manual. Subscribing requires the creator to attest the consent basis via reason; unsubscribing takes no reason. Clients already in the target status are reported as unchanged.
Set the marketing-email subscription of MANY clients at once (the Customers-list "Subscribe" / "Unsubscribe" bulk actions). Source manual. Subscribing requires the creator to attest the consent basis via reason; unsubscribing takes no reason. Clients already in the target status are reported as unchanged.
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/email-subscription" \ -H "Content-Type: application/json" \ -d '{ "status": "subscribed", "reason": "imported-from-other-list", "clientIds": [ "string" ] }'{ "ok": true, "updatedClientIds": [ "string" ], "unchangedClientIds": [ "string" ], "skippedClientIds": [ "string" ]}{ "error": "string", "message": "string"}Bulk subscribe/unsubscribe clients (SMS)
/clients/sms-subscriptionAuthorization
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.
Set the marketing-SMS consent of MANY clients at once (the Customers-list "Subscribe to SMS" / "Unsubscribe from SMS" bulk actions). Source manual. Separate from the e-mail twin on purpose: SMS is its own consent under PL/EU rules, so an e-mail opt-in is never a basis here and the reason list is narrower (ClientSmsSubscriptionReason). Subscribing requires the creator to attest the basis; unsubscribing takes no reason.
Set the marketing-SMS consent of MANY clients at once (the Customers-list "Subscribe to SMS" / "Unsubscribe from SMS" bulk actions). Source manual. Separate from the e-mail twin on purpose: SMS is its own consent under PL/EU rules, so an e-mail opt-in is never a basis here and the reason list is narrower (ClientSmsSubscriptionReason). Subscribing requires the creator to attest the basis; unsubscribing takes no reason.
Response Body
application/json
application/json
curl -X POST "https://example.com/clients/sms-subscription" \ -H "Content-Type: application/json" \ -d '{ "status": "subscribed", "reason": "explicit-sms-consent", "clientIds": [ "string" ] }'{ "ok": true, "updatedClientIds": [ "string" ], "unchangedClientIds": [ "string" ], "skippedClientIds": [ "string" ]}{ "error": "string", "message": "string"}Was this article helpful?

