Workspace API

Clients

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

13 min readLast updated Sep 10, 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",      "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)

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",  "message": "string"}

List client tags

GET/clients/tags

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/tags"
{  "count": 0,  "tags": [    {      "id": "string",      "name": "string",      "color": "string"    }  ]}
{  "error": "string",  "message": "string"}

Export clients as CSV

GET/clients/export

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Query Parameters

columns?string

Comma-separated optional column groups, any subset of: tags, stats, emailSubscription, activity, accesses, invoiceData, attribution, identifiers. Omit for identity columns only.

search?string

Case-insensitive prefix match on email / first name / last name.

products?string

has = only clients with active product access, no = only clients without any.

createdFrom?string

Only clients added on/after this ISO date (inclusive).

createdTo?string

Only clients added on/before this ISO date (inclusive).

limit?string

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

POST/clients/bulk-filtered-action

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.

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

POST/clients/delete

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.

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

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

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",  "message": "string"}

List a client's product accesses

GET/clients/{clientId}/product-accesses

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Path Parameters

clientId*string

Client id

Query Parameters

status?string

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

POST/products/{productId}/clients/bulk-action

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.

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

GET/products/{productId}/clients

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Path Parameters

productId*string

Product id

Query Parameters

accessStatus?string

Filter on the derived access status; all returns every grant. Default: active.

Value in

  • "active"
  • "revoked"
  • "expired"
  • "all"
status?string

Legacy: stored grant status (archived = revoked + expired). Ignored when accessStatus is sent.

Value in

  • "active"
  • "archived"
limit?integer

Page size, max 100 (default 50).

cursor?string

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

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",  "message": "string"}

List a product's plan-bundled access holders

GET/products/{productId}/clients/bundled-access

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Path Parameters

productId*string

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

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",  "message": "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",  "message": "string"}

Cancel a client's subscription

POST/products/{productId}/clients/cancel-subscription

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.

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

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",  "message": "string"}

Add a 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",  "message": "string"}

Client email delivery log

GET/clients/{clientId}/email-log

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Path Parameters

clientId*string

Client id

Query Parameters

limit?integer

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

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",  "message": "string"}

List a client's devices

GET/clients/{clientId}/devices

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/devices"
{  "devices": [    {      "deviceId": "string",      "clientKind": "web",      "deviceName": "string",      "createdAt": "string",      "lastSeen": "string",      "activeUntil": "string",      "isCurrent": true    }  ]}
{  "error": "string",  "message": "string"}

Revoke a client device

POST/clients/{clientId}/devices/{deviceId}/revoke

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Path Parameters

clientId*string

Client id

deviceId*string

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

POST/clients/{clientId}/devices/reset

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 POST "https://example.com/clients/string/devices/reset"
{  "revokedCount": 0}
{  "error": "string",  "message": "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",  "message": "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",  "message": "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",  "message": "string"}

Was this article helpful?

Related articles

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