Workspace API

Clients

Clients endpoints of the Zanfia Workspace API, with schemas and code samples.

7 min readLast updated Jul 26, 2026

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

GET/clients

Authorization

bearerAuth
AuthorizationBearer <token>

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",      "createdAt": "string"    }  ],  "nextCursor": "string"}
{  "error": "string"}

Add a client (contact only)

POST/clients

Authorization

bearerAuth
AuthorizationBearer <token>

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

Client detail

GET/clients/{clientId}

Authorization

bearerAuth
AuthorizationBearer <token>

API key from Dashboard → Integrations → API, MCP and CLI.

In: header

Path Parameters

clientId*string

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",  "createdAt": "string",  "previousEmails": [    "string"  ],  "firstSeen": "string",  "lastSeen": "string"}
{  "error": "string"}

Update a client

PATCH/clients/{clientId}

Authorization

bearerAuth
AuthorizationBearer <token>

API key from Dashboard → Integrations → API, MCP and CLI.

In: header

Path Parameters

clientId*string

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

Grant product access

POST/products/{productId}/clients

Authorization

bearerAuth
AuthorizationBearer <token>

API key from Dashboard → Integrations → API, MCP and CLI.

In: header

Path Parameters

productId*string

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

Revoke product access

POST/products/{productId}/clients/revoke-access

Authorization

bearerAuth
AuthorizationBearer <token>

API key from Dashboard → Integrations → API, MCP and CLI.

In: header

Path Parameters

productId*string

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

Change an access period

POST/products/{productId}/clients/access-period

Authorization

bearerAuth
AuthorizationBearer <token>

API key from Dashboard → Integrations → API, MCP and CLI.

In: header

Path Parameters

productId*string

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

List client notes

GET/clients/{clientId}/notes

Authorization

bearerAuth
AuthorizationBearer <token>

API key from Dashboard → Integrations → API, MCP and CLI.

In: header

Path Parameters

clientId*string

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

Add client note

POST/clients/{clientId}/notes

Authorization

bearerAuth
AuthorizationBearer <token>

API key from Dashboard → Integrations → API, MCP and CLI.

In: header

Path Parameters

clientId*string

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

Change a client's email

POST/clients/{clientId}/change-email

Authorization

bearerAuth
AuthorizationBearer <token>

API key from Dashboard → Integrations → API, MCP and CLI.

In: header

Path Parameters

clientId*string

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

Bulk-import contacts

POST/clients/import

Authorization

bearerAuth
AuthorizationBearer <token>

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

Preview a client merge

GET/clients/merge-preview

Authorization

bearerAuth
AuthorizationBearer <token>

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

Merge duplicate clients

POST/clients/merge

Authorization

bearerAuth
AuthorizationBearer <token>

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

Was this article helpful?

Related articles

Spotted something off? Tell us at support@zanfia.com.