Workspace API

Products

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

14 min readLast updated Oct 24, 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",  "message": "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",  "message": "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"    }  ],  "storefrontVisibility": "public",  "language": "pl"}
{  "error": "string",  "message": "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",  "message": "string"}

Delete product

DELETE/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 DELETE "https://example.com/products/string"
{  "productId": "string",  "status": "draft",  "purgeAfter": 0,  "runId": "string",  "activeGrants": 0,  "cancellableSubscriptions": 0}
{  "error": "string",  "message": "string"}

Duplicate a product

POST/products/{productId}/duplicate

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/duplicate — copy a product as a new DRAFT: its settings, description, plans, every non-archived offer (fresh price ids, never shared with the source) and its content (course sections, lessons and quizzes; downloads; event / consultation / service config). Customers, orders, access and stats are not copied; the slug gets a -kopia (Polish product) or -copy suffix. Community, knowledge base and newsletter products answer 422 unsupported-product-type.

Response Body

application/json

application/json

curl -X POST "https://example.com/products/string/duplicate" \  -H "Content-Type: application/json" \  -d '{}'
{  "id": "string",  "name": "string",  "slug": "string",  "productType": "course",  "status": "draft",  "isFree": true,  "createdAt": "string",  "defaultCheckoutId": "string",  "skippedCheckoutIds": [    "string"  ]}
{  "error": "string",  "message": "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",  "message": "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",  "message": "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,    "sessionsPerPurchase": 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,    "liveRoomId": "string"  }}
{  "error": "string",  "message": "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",  "message": "string"}

List consultation bookings

GET/consultations/bookings

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Query Parameters

productId?string

One consultation product; omit for every consultation of the workspace.

from?string

Range start (inclusive): ISO 8601 date-time or YYYY-MM-DD (UTC). Default: today 00:00 UTC.

to?string

Range end (exclusive): ISO 8601 date-time or YYYY-MM-DD (= end of that day, UTC). Default: from + 30 days.

status?string

booked = not ended yet, done = already ended, cancelled. Omit for all.

Value in

  • "booked"
  • "cancelled"
  • "done"
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/consultations/bookings"
{  "items": [    {      "bookingId": "string",      "startAt": "string",      "endAt": "string",      "durationMinutes": 0,      "clientEmail": "string",      "clientName": "string",      "productId": "string",      "productName": "string",      "status": "booked",      "clientId": "string",      "clientTimezone": "string",      "timezone": "string",      "location": "string",      "locationDetails": "string",      "bookedAt": "string",      "cancelledAt": "string",      "cancelledBy": "creator"    }  ],  "nextCursor": "string",  "from": "string",  "to": "string"}
{  "error": "string",  "message": "string"}

List consultation buyers without a booking

GET/consultations/clients-without-booking

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Query Parameters

productId?string

One consultation product; omit for every consultation of the workspace.

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/consultations/clients-without-booking"
{  "items": [    {      "productId": "string",      "productName": "string",      "clientId": "string",      "clientName": "string",      "clientEmail": "string",      "accessGrantedAt": "string"    }  ],  "nextCursor": "string"}
{  "error": "string",  "message": "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",      "appendEmailSha256": true,      "custom": {        "eyebrow": "string",        "headline": "string",        "messageHtml": "string",        "messageMarkdown": "string",        "ctaLabel": "string",        "ctaUrl": "string",        "showOrderSummary": true,        "nextOffer": {          "kind": "checkout",          "checkoutId": "string",          "productId": "string",          "headline": "string",          "description": "string",          "ctaLabel": "string"        }      }    },    "emails": {      "sendPurchaseConfirmationToBuyer": true,      "sendPurchaseNotificationToOwner": true,      "sendFailedPaymentEmailToBuyer": true,      "sendFailedPaymentEmailToOwner": true    },    "emailOverrides": {      "property1": {        "enabled": true,        "subject": {          "en": "string",          "pl": "string"        },        "headline": {          "en": "string",          "pl": "string"        },        "layout": [          {            "id": "string",            "type": "text",            "text": {              "en": "string",              "pl": "string"            }          }        ],        "body": {          "en": "string",          "pl": "string"        },        "variants": {          "property1": {},          "property2": {}        },        "attachments": [          {            "kind": "media",            "mediaId": "string",            "name": "string",            "mimeType": "string",            "size": 0,            "storagePath": "string",            "url": "string"          }        ]      },      "property2": {        "enabled": true,        "subject": {          "en": "string",          "pl": "string"        },        "headline": {          "en": "string",          "pl": "string"        },        "layout": [          {            "id": "string",            "type": "text",            "text": {              "en": "string",              "pl": "string"            }          }        ],        "body": {          "en": "string",          "pl": "string"        },        "variants": {          "property1": {},          "property2": {}        },        "attachments": [          {            "kind": "media",            "mediaId": "string",            "name": "string",            "mimeType": "string",            "size": 0,            "storagePath": "string",            "url": "string"          }        ]      }    }  }}
{  "error": "string",  "message": "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",  "message": "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",  "message": "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",  "message": "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",  "message": "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). Checkout prices still linked to a removed plan are unlinked in the same call (they resolve to the default plan); upgrade offers touching it are deactivated.

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",  "message": "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",  "message": "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. Answers 202 at once; a background run adds the members.

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"  }'
{  "status": "started",  "eligibleClients": 0,  "runId": "string"}
{  "error": "string",  "message": "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",  "message": "string"}

Rename download file

PATCH/products/{productId}/downloads/files/{uid}

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Path Parameters

productId*string

Product id

uid*string

Download file uid

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

New label of one attached file; it is also the buyer's download filename (max 255 characters).

Response Body

application/json

application/json

curl -X PATCH "https://example.com/products/string/downloads/files/string" \  -H "Content-Type: application/json" \  -d '{    "name": "string"  }'
{  "file": {    "url": "string",    "name": "string",    "uid": "string",    "mediaId": "string",    "size": 0  },  "files": [    {      "url": "string",      "name": "string",      "uid": "string",      "mediaId": "string",      "size": 0    }  ]}
{  "error": "string",  "message": "string"}

Get product file set

GET/products/{productId}/file-set

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/file-set"
{  "productId": "string",  "revision": 0,  "fileCount": 0,  "totalBytes": 0,  "lastMessage": "string",  "updatedAt": "string",  "files": [    {      "path": "string",      "size": 0,      "contentType": "string",      "sha256": "string",      "updatedAt": "string"    }  ]}
{  "error": "string",  "message": "string"}

Change product file set

POST/products/{productId}/file-set

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/file-set on the Workspace API.

Response Body

application/json

application/json

curl -X POST "https://example.com/products/string/file-set" \  -H "Content-Type: application/json" \  -d '{}'
{  "revision": 0,  "added": [    "string"  ],  "updated": [    "string"  ],  "unchanged": [    "string"  ],  "deleted": [    "string"  ],  "fileCount": 0,  "totalBytes": 0}
{  "error": "string",  "message": "string"}

Preview a notification to a product's buyers

POST/products/{productId}/buyers-notifications/preview

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/buyers-notifications/preview" \  -H "Content-Type: application/json" \  -d '{}'
{  "recipientCount": 0,  "link": "string",  "canSend": true,  "blockedBy": "product_cooldown",  "nextAllowedAt": "string",  "sentToday": 0,  "dailyCap": 0,  "emailSkipped": "email_limit_reached",  "emailRemaining": 0,  "emailResetsAt": "string"}
{  "error": "string",  "message": "string"}

List a product's buyer notifications

GET/products/{productId}/buyers-notifications

Authorization

bearerAuth
AuthorizationBearer <token>

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

In: header

Path Parameters

productId*string

Product id

Query Parameters

limit?integer

Newest first; default 20, max 50.

Range1 <= value <= 50

Response Body

application/json

application/json

curl -X GET "https://example.com/products/string/buyers-notifications"
{  "items": [    {      "id": "string",      "productId": "string",      "productName": "string",      "title": "string",      "body": "string",      "link": "string",      "sendEmail": true,      "emailSkipped": "email_limit_reached",      "status": "queued",      "createdAt": "string",      "finishedAt": "string",      "confirmedRecipientCount": 0,      "counts": {        "recipients": 0,        "bells": 0,        "pushes": 0,        "emails": 0,        "optedOut": 0      }    }  ]}
{  "error": "string",  "message": "string"}

Notify a product's buyers

POST/products/{productId}/buyers-notifications

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/buyers-notifications" \  -H "Content-Type: application/json" \  -d '{    "sendId": "string",    "title": "string",    "body": "string",    "sendEmail": true,    "confirmedRecipientCount": 0  }'
{  "notification": {    "id": "string",    "productId": "string",    "productName": "string",    "title": "string",    "body": "string",    "link": "string",    "sendEmail": true,    "emailSkipped": "email_limit_reached",    "status": "queued",    "createdAt": "string",    "finishedAt": "string",    "confirmedRecipientCount": 0,    "counts": {      "recipients": 0,      "bells": 0,      "pushes": 0,      "emails": 0,      "optedOut": 0    }  },  "alreadySent": true}
{  "error": "string",  "message": "string"}

Was this article helpful?

Related articles

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