Workspace API

Products

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

9 min readLast updated Jul 26, 2026

Create, edit, publish, and price products. All routes require a Authorization: Bearer header — see Authentication.

List products

GET/products

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/products"
{  "count": 0,  "products": [    {      "id": "string",      "name": "string",      "slug": "string",      "productType": "course",      "status": "draft",      "isFree": true,      "createdAt": "string"    }  ]}
{  "error": "string"}

Create a product

POST/products

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.

Response Body

application/json

application/json

curl -X POST "https://example.com/products" \  -H "Content-Type: application/json" \  -d '{    "name": "string",    "productType": "course"  }'
{  "id": "string",  "name": "string",  "slug": "string",  "productType": "course",  "status": "draft",  "isFree": true,  "createdAt": "string",  "defaultCheckoutId": "string"}
{  "error": "string"}

Product detail

GET/products/{productId}

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"
{  "id": "string",  "name": "string",  "slug": "string",  "productType": "course",  "status": "draft",  "isFree": true,  "createdAt": "string",  "description": "string",  "clientCount": 0,  "plans": [    {      "id": "string",      "name": "string",      "description": "string",      "active": true,      "isDefault": true,      "order": 0,      "includedProducts": [        {          "productId": "string",          "productName": "string",          "productType": "course"        }      ],      "communityRoleGrants": {        "property1": [          "string"        ],        "property2": [          "string"        ]      },      "createdAt": "string",      "updatedAt": "string"    }  ]}
{  "error": "string"}

Update product info

PATCH/products/{productId}

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.

Partial update of a product's display info. Omitted fields are left unchanged.

Response Body

application/json

application/json

curl -X PATCH "https://example.com/products/string" \  -H "Content-Type: application/json" \  -d '{}'
{  "productId": "string"}
{  "error": "string"}

Archive a product

POST/products/{productId}/archive

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 POST "https://example.com/products/string/archive"
{  "productId": "string",  "status": "draft"}
{  "error": "string"}

Publish or unpublish a product

POST/products/{productId}/status

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.

Response Body

application/json

application/json

curl -X POST "https://example.com/products/string/status" \  -H "Content-Type: application/json" \  -d '{    "status": "draft"  }'
{  "productId": "string",  "status": "draft"}
{  "error": "string"}

Appointment settings

GET/products/{productId}/appointment

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/appointment"
{  "productType": "consultation",  "consultation": {    "bookingMethod": "native",    "bookingUrl": "string",    "duration": 0,    "location": "string",    "locationDetails": "string",    "confirmationMessage": "string",    "showOnSuccessPage": true,    "availability": [      {        "dayOfWeek": 0,        "startTime": "string",        "endTime": "string"      }    ],    "exceptions": [      "string"    ],    "bufferMinutes": 0,    "maxBookingsPerDay": 0,    "maxBookingsPerWeek": 0,    "minNoticeHours": 0,    "bookingWindowDays": 0,    "timezone": "string"  },  "service": {    "deliverables": [      "string"    ],    "deliveryTime": {      "value": 0,      "unit": "days"    },    "revisionRounds": 0,    "briefMethod": "native",    "briefUrl": "string",    "showOnSuccessPage": true,    "briefFields": [      {        "id": "string",        "label": "string",        "type": "shortText",        "required": true,        "options": "string"      }    ]  },  "event": {    "type": "online",    "location": "string",    "startsAt": "string",    "endsAt": "string",    "capacity": 0,    "showOnSuccessPage": true  }}
{  "error": "string"}

Update appointment settings

PATCH/products/{productId}/appointment

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.

Upsert an appointment product's content config. Provide EXACTLY ONE of the three sub-objects, and it must match the product's own type (else 400/409).

Response Body

application/json

application/json

curl -X PATCH "https://example.com/products/string/appointment" \  -H "Content-Type: application/json" \  -d '{}'
{  "updated": true}
{  "error": "string"}

After-purchase settings

GET/products/{productId}/after-purchase

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/after-purchase"
{  "settings": {    "thankYou": {      "type": "defaultPage",      "redirectUrl": "string",      "custom": {        "eyebrow": "string",        "headline": "string",        "messageHtml": "string",        "messageMarkdown": "string",        "ctaLabel": "string",        "ctaUrl": "string",        "showOrderSummary": true      }    },    "emails": {      "sendPurchaseConfirmationToBuyer": true,      "sendPurchaseNotificationToOwner": true,      "sendFailedPaymentEmailToBuyer": true,      "sendFailedPaymentEmailToOwner": true    },    "purchaseEmail": {      "purchase": {        "subject": "string",        "headline": "string",        "messageHtml": "string",        "messageMarkdown": "string"      },      "free": {        "subject": "string",        "headline": "string",        "messageHtml": "string",        "messageMarkdown": "string"      },      "trial": {        "subject": "string",        "headline": "string",        "messageHtml": "string",        "messageMarkdown": "string"      }    },    "renewalReminderEmail": {      "subject": "string",      "headline": "string",      "messageHtml": "string",      "messageMarkdown": "string"    }  }}
{  "error": "string"}

Update after-purchase settings

PATCH/products/{productId}/after-purchase

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.

Partial write of the product's shared after-purchase defaults. Omitted sections keep their stored value (the defaults are configured progressively). Written sections are mirrored onto every non-overriding offer of the product.

Response Body

application/json

application/json

curl -X PATCH "https://example.com/products/string/after-purchase" \  -H "Content-Type: application/json" \  -d '{}'
{  "updated": true}
{  "error": "string"}

Replace product images

PUT/products/{productId}/images

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.

Full replace of a product's cover image array. Each entry must be an https URL ALREADY hosted (upload to the Media Library first — POST /media — and use its returned url; this route only writes the product's images array and best-effort syncs them to the linked Stripe Product). Certificate / generated cover fields are not editable here.

Response Body

application/json

application/json

curl -X PUT "https://example.com/products/string/images" \  -H "Content-Type: application/json" \  -d '{    "images": [      "string"    ]  }'
{  "images": [    "string"  ]}
{  "error": "string"}

List product prices

GET/products/{productId}/prices

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/prices"
{  "count": 0,  "prices": [    {      "id": "string",      "productId": "string",      "amountCents": 0,      "currency": "string",      "type": "one_time",      "active": true,      "status": "available",      "gateways": [        "string"      ],      "label": "string"    }  ]}
{  "error": "string"}

Add a product price

POST/products/{productId}/prices

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.

Create a one-time, single-currency paid price on the product's default checkout (creating the checkout if none exists). The Stripe price is created server-side; if no payment gateway is connected yet the price is stored unregistered (empty gateways) and registered when the offer is published.

Response Body

application/json

application/json

curl -X POST "https://example.com/products/string/prices" \  -H "Content-Type: application/json" \  -d '{    "amountCents": 0,    "currency": "string"  }'
{  "id": "string",  "productId": "string",  "amountCents": 0,  "currency": "string",  "type": "one_time",  "active": true,  "status": "available",  "gateways": [    "string"  ],  "label": "string"}
{  "error": "string"}

Replace plans & bundling

PUT/products/{productId}/plans

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.

Full replace of a product's plans[] (plans & bundling). Mirrors the internal UpdateProductPlansRequestDTO: the caller sends the complete desired state; the server generates ids for new plans, owns timestamps, and enforces the invariants (unique ids, at most one default, no self-include, one level of bundling both directions, and a delete guard when active checkout prices still reference a removed plan).

Response Body

application/json

application/json

curl -X PUT "https://example.com/products/string/plans" \  -H "Content-Type: application/json" \  -d '{    "plans": [      {        "name": "string",        "active": true,        "includedProducts": [          {            "productId": "string",            "productName": "string"          }        ]      }    ]  }'
{  "productId": "string",  "plans": [    {      "id": "string",      "name": "string",      "description": "string",      "active": true,      "isDefault": true,      "order": 0,      "includedProducts": [        {          "productId": "string",          "productName": "string",          "productType": "course"        }      ],      "communityRoleGrants": {        "property1": [          "string"        ],        "property2": [          "string"        ]      },      "createdAt": "string",      "updatedAt": "string"    }  ]}
{  "error": "string"}

Count clients per plan

POST/products/{productId}/plans/count-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.

Count the product's active customers whose EFFECTIVE plan is planId (grant match, falling back to the default plan — the same resolution the access checks use). Omit planId to size a not-yet-saved first/default plan: every active grant counts.

Response Body

application/json

application/json

curl -X POST "https://example.com/products/string/plans/count-clients" \  -H "Content-Type: application/json" \  -d '{}'
{  "count": 0}
{  "error": "string"}

Backfill plan community members

POST/products/{productId}/plans/backfill-community-members

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.

Eagerly create community-member docs for every active customer whose effective plan includes communityProductId — sends membership grants, so run it deliberately (typically right after adding a community to a plan). Idempotent: existing members are left untouched.

Response Body

application/json

application/json

curl -X POST "https://example.com/products/string/plans/backfill-community-members" \  -H "Content-Type: application/json" \  -d '{    "communityProductId": "string"  }'
{  "added": 0,  "alreadyMembers": 0,  "skipped": 0}
{  "error": "string"}

Change a client's plan

POST/products/{productId}/clients/plan

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.

Move a client's grant to a different plan on a multi-plan product. Billing-neutral: only grant.planId moves; community membership is reconciled server-side as part of the same request.

Response Body

application/json

application/json

curl -X POST "https://example.com/products/string/clients/plan" \  -H "Content-Type: application/json" \  -d '{    "clientId": "string",    "planId": "string"  }'
{  "clientId": "string",  "planId": "string",  "membership": {    "added": 0,    "alreadyMembers": 0,    "skipped": 0,    "removed": 0  }}
{  "error": "string"}

Was this article helpful?

Related articles

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