Create product
What this does: Creates a new catalog product with variants and options. Catalog products are standardized, reusable offerings that can be added to projects, tickets, quotes, and invoices.
Product structure: A product consists of:
- Product details: Name, category, logo, description, and base pricing
- Variants (optional): Different configurations or performance levels (e.g., Standard, Pro, Enterprise) with individual prices
- Options (optional): Extra services or add-ons that customers can optionally select (e.g., “Add workshop” or “Enable premium support”)
Pricing: You can set one or more pricing types:
- Fixed price: One-time payment (e.g., for hardware or one-time services)
- Subscription price: Recurring payment (e.g., monthly SaaS subscription)
- Price per unit: Flexible billing by quantity (e.g., per user, per hour)
Description format:
- Descriptions can be provided as HTML or Markdown
- The system automatically converts them to internal format (IDoc) for storage
- Supported formatting: tables, callouts, emojis, and basic text formatting
Logo upload: To add a logo to your product:
- First, call POST /api/public/workspace/upload to upload the image file
- The upload endpoint returns a file ID
- Use that file ID in the logoId field when creating the product
Validation:
- Product name is required
- Category ID must be a valid UUID
- At least one variant must be provided (even if just one)
- Options array is required (can be empty)
- If pricePerUnit is set, priceUnitName is required
- If pricePerUnit or priceSubscription is set, priceFrequency must be 1, 3, 6, or 12 (months)
Response: Returns the created product with all details, including:
- Descriptions converted to HTML
- Logo URL (if logo was uploaded)
- All variants and options with their configurations
POST
/administration/product-catalogAuthorization
AuthorizationOAuth2 access token · headerrequiredScopes:
api:writeor
AuthorizationBearer token (JWT) · headerrequiredQuery parameters
fieldsToReturnstringComma-separated list of top-level response fields to return. Overrides the endpoint compact default unless responseShape=full is used.
responseShapestringAdvanced override. Omit for the endpoint compact default. Use full only when you need the complete endpoint response, including nested fields that are not selectable with fieldsToReturn.
Allowed:
compactfullHeader parameters
LT-Response-ShapestringAdvanced override. Set to full only when you need the complete endpoint response, including nested fields that are not selectable with fieldsToReturn.
Allowed:
fullRequest body
requiredapplication/jsoncategoryIdstringrequiredUUID of the product category this product belongs to. Categories help organize products into groups (e.g., "Hardware", "Software", "Services").
contractPeriodMonthsnumberContract period in months. Required when pricePerUnit or priceSubscription is greater than 0. Defaults to 12 months if not specified.
descriptionstringrequiredProduct description explaining what the product includes, its benefits, and key features. Accepts Markdown or HTML for complex formatting. This content is rendered by the Leadtime rich editor, not by a plain Markdown viewer.
For quick/simple text, Markdown is acceptable. For polished user-facing content from an agent or integration, prefer structured HTML because it preserves editor blocks, links, and layout more predictably.
Use only the nodes and marks documented below for this field. Supported editor features differ by endpoint and field. Choose formatting for readability, not decoration: use headings for real sections, paragraphs for narrative text, lists or tables for structured facts, blockquotes for quoted context, callouts for important outcomes/risks/notes when supported, and explicit anchors for links. Do not rely on bare URLs or Markdown links when the link must be clickable; use explicit anchors such as <a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">link text</a>.
In HTML you can use:
## Node Types and Marks
### Nodes
**paragraph**
Type: block
Content: inline* (inline only — do not use block tags such as <p> inside; use e.g. <br>, <strong>, <em>, <span>…)
Default paragraph. extraPlaceholder is an internal structure for the editor to show a ghost placeholder in empty “template” lines (ProseMirror JSON fragment or null). For normal agent output leave extraPlaceholder as null. Only set it if you are intentionally mirroring a field placeholder the user already has in the open editor; do not set random placeholder data for free-form answers.
Atts:
- extraPlaceholder: null
```html
<p class="paragraph-base" extraplaceholder="null/some-value"></p>
```
**heading**
Type: block
Content: inline* (inline only — do not use block tags such as <p> inside; use e.g. <br>, <strong>, <em>, <span>…)
Atts:
- level: 1
```html
<h1 class="heading-base"></h1>
```
**bulletList**
Type: block list
Content: listItem+
Atts: none
```html
<ul class="list-base"><li></li></ul>
```
**hardBreak**
Type: inline
Atts: none
```html
<br>
```
**horizontalRule**
Type: block
Atts: none
```html
<hr class="hr-base">
```
**orderedList**
Type: block list
Content: listItem+
Atts:
- start: 1
- type: null
```html
<ol class="order-list-base" type="null/some-value"><li></li></ol>
```
**listItem**
Content: (paragraph|list)* (paragraphs and/or lists, in any order)
Atts: none
```html
<li></li>
```
**blockquote**
Type: block
Content: block+ (one or more block child nodes (e.g. paragraph, list, …))
Atts: none
```html
<blockquote class="blockquote-base"><p class="paragraph-base"></p></blockquote>
```
**table**
Type: block
Content: tableRow+
Atts: none
```html
<table style="width: 0px;"><colgroup></colgroup><tbody><tr></tr></tbody></table>
```
**tableRow**
Content: (tableCell | tableHeader)*
Atts: none
```html
<tr></tr>
```
**tableHeader**
Content: block+ (one or more block child nodes (e.g. paragraph, list, …))
Atts:
- colspan: 1
- rowspan: 1
- colwidth: null
- align: null
```html
<th colspan="1" rowspan="1" colwidth="null/some-value" style="text-align: null/some-value;"><p class="paragraph-base"></p></th>
```
**tableCell**
Content: block+ (one or more block child nodes (e.g. paragraph, list, …))
Atts:
- colspan: 1
- rowspan: 1
- colwidth: null
- align: null
```html
<td colspan="1" rowspan="1" colwidth="null/some-value" style="text-align: null/some-value;"><p class="paragraph-base"></p></td>
```
**callout**
Type: block
Content: block+ (one or more block child nodes (e.g. paragraph, list, …))
Callout / info box. The icon attr is an emoji shortcode (e.g. :information_source:, :warning:); it appears on the callout. Body is block content (paragraphs, lists, etc.) — use normal block HTML such as <p class="paragraph-base"> inside the callout <div>. data-type is "callout".
Atts:
- icon: :information_source:
```html
<div data-type="callout" icon=":information_source:"><p class="paragraph-base"></p></div>
```
**emoji**
Type: inline
Inline emoji. icon is a colon shortcode (e.g. :thumbsup:, :white_check_mark:) stored in data-icon; body is often empty. Use names your emoji set supports; avoid inventing invalid shortcodes in final HTML if you need them to render.
Atts:
- icon: :smile:
```html
<span data-type="emoji" data-icon=":smile:"></span>
```
### Marks
**link**
Atts:
- href: null
- target: _blank
- rel: noopener noreferrer nofollow
- class: null
- title: null
```html
<a target="_blank" rel="noopener noreferrer nofollow" href="">link text example</a>
```
**bold**
Atts: none
```html
<strong>bold text example</strong>
```
**code**
Atts: none
```html
<code>code text example</code>
```
**italic**
Atts: none
```html
<em>italic text example</em>
```
**strike**
Atts: none
```html
<s>strike text example</s>
```
**underline**
Atts: none
```html
<u>underline text example</u>
```
logoIdobject | nullFile ID of the product logo image. To upload a logo, first call POST /api/public/workspace/upload to upload the image file, then use the returned file ID here. Set to null if no logo is needed.
namestringrequiredName of the product or service. This is the primary identifier shown in the catalog, quotes, and invoices.
optionsProductOptionApiDto[]requiredList of product options. Options are extra services or add-ons that customers can optionally select (e.g., "Add workshop", "Enable premium support"). Options are organized into groups with multiple selectable values. Can be an empty array if no options are needed.
Show propertiesHide properties
Array of
ProductOptionApiDtoidstringOption ID. Include this when updating an existing option. Omit when creating a new option.
isMultiplebooleanrequiredWhether customers can select multiple values from this option. If true, customers can select multiple values (e.g., "Add workshop" AND "Enable premium support"). If false, only one value can be selected.
isRequiredbooleanrequiredWhether this option must be selected. If true, customers must choose at least one value from this option before proceeding.
namestringrequiredName of the option group (e.g., "Additional Services", "Storage Options", "Support Level"). This groups related option values together.
valuesProductOptionValueApiDto[]requiredList of available values for this option. Each value can have its own pricing. At least one value must be provided.
Show propertiesHide properties
Array of
ProductOptionValueApiDtoextraPriceFixedobject | nullAdditional one-time fixed price added when this option value is selected. Use this for one-time add-ons or upgrades.
extraPricePerUnitobject | nullAdditional per-unit price added when this option value is selected. Use this for quantity-based add-ons.
extraPriceSubscriptionobject | nullAdditional recurring subscription price added when this option value is selected. Use this for recurring add-ons (e.g., monthly premium support).
idstringOption value ID. Include this when updating an existing option value. Omit when creating a new option value.
namestringrequiredName of the option value (e.g., "Add workshop", "Enable premium support", "Extra 10GB storage"). This is what customers see when selecting options.
priceFixedobject | nullOne-time fixed price for the product. Use this for one-time purchases (e.g., hardware, installation fees, one-time services). Can be combined with subscription or per-unit pricing. Use 0 or null when the product has no fixed charge.
priceFrequencynumberBilling frequency in months for subscription or per-unit pricing. Required when pricePerUnit or priceSubscription is greater than 0. Valid values: 1 (monthly), 3 (quarterly), 6 (semi-annually), 12 (annually).
Allowed:
13612pricePerUnitobject | nullPrice per unit for flexible quantity-based billing. Use this when pricing depends on quantity (e.g., per user, per hour, per license, per GB). Requires priceUnitName to be set. Use 0 or null when the product has no per-unit charge.
priceSubscriptionobject | nullRecurring subscription price for the product. Use this for recurring revenue models (e.g., monthly SaaS subscription, annual maintenance). The billing frequency is controlled by priceFrequency. Use 0 or null when the product has no subscription charge.
priceUnitNamestringName of the unit for per-unit pricing (e.g., "user", "hour", "license", "GB"). Required when pricePerUnit is greater than 0. This is displayed in quotes and invoices.
renewalModestringRenewal mode for the product subscription. Automatic means the subscription will automatically renew, Stops means it will stop at the end of the contract period. Required when pricePerUnit or priceSubscription is greater than 0. Defaults to Automatic.
Allowed:
AutomaticStopsvariantsProductVariantApiDto[]requiredList of product variants. Variants represent different configurations or performance levels of the same product (e.g., Standard, Pro, Enterprise). Each variant can have its own pricing and description. At least one variant must be provided.
Show propertiesHide properties
Array of
ProductVariantApiDtodescriptionobject | nullrequiredVariant description explaining what this variant includes or offers. Accepts Markdown or HTML for complex formatting. This content is rendered by the Leadtime rich editor, not by a plain Markdown viewer.
For quick/simple text, Markdown is acceptable. For polished user-facing content from an agent or integration, prefer structured HTML because it preserves editor blocks, links, and layout more predictably.
Use only the nodes and marks documented below for this field. Supported editor features differ by endpoint and field. Choose formatting for readability, not decoration: use headings for real sections, paragraphs for narrative text, lists or tables for structured facts, blockquotes for quoted context, callouts for important outcomes/risks/notes when supported, and explicit anchors for links. Do not rely on bare URLs or Markdown links when the link must be clickable; use explicit anchors such as <a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">link text</a>.
In HTML you can use:
## Node Types and Marks
### Nodes
**paragraph**
Type: block
Content: inline* (inline only — do not use block tags such as <p> inside; use e.g. <br>, <strong>, <em>, <span>…)
Default paragraph. extraPlaceholder is an internal structure for the editor to show a ghost placeholder in empty “template” lines (ProseMirror JSON fragment or null). For normal agent output leave extraPlaceholder as null. Only set it if you are intentionally mirroring a field placeholder the user already has in the open editor; do not set random placeholder data for free-form answers.
Atts:
- extraPlaceholder: null
```html
<p class="paragraph-base" extraplaceholder="null/some-value"></p>
```
**heading**
Type: block
Content: inline* (inline only — do not use block tags such as <p> inside; use e.g. <br>, <strong>, <em>, <span>…)
Atts:
- level: 1
```html
<h1 class="heading-base"></h1>
```
**bulletList**
Type: block list
Content: listItem+
Atts: none
```html
<ul class="list-base"><li></li></ul>
```
**hardBreak**
Type: inline
Atts: none
```html
<br>
```
**horizontalRule**
Type: block
Atts: none
```html
<hr class="hr-base">
```
**orderedList**
Type: block list
Content: listItem+
Atts:
- start: 1
- type: null
```html
<ol class="order-list-base" type="null/some-value"><li></li></ol>
```
**listItem**
Content: (paragraph|list)* (paragraphs and/or lists, in any order)
Atts: none
```html
<li></li>
```
**blockquote**
Type: block
Content: block+ (one or more block child nodes (e.g. paragraph, list, …))
Atts: none
```html
<blockquote class="blockquote-base"><p class="paragraph-base"></p></blockquote>
```
**table**
Type: block
Content: tableRow+
Atts: none
```html
<table style="width: 0px;"><colgroup></colgroup><tbody><tr></tr></tbody></table>
```
**tableRow**
Content: (tableCell | tableHeader)*
Atts: none
```html
<tr></tr>
```
**tableHeader**
Content: block+ (one or more block child nodes (e.g. paragraph, list, …))
Atts:
- colspan: 1
- rowspan: 1
- colwidth: null
- align: null
```html
<th colspan="1" rowspan="1" colwidth="null/some-value" style="text-align: null/some-value;"><p class="paragraph-base"></p></th>
```
**tableCell**
Content: block+ (one or more block child nodes (e.g. paragraph, list, …))
Atts:
- colspan: 1
- rowspan: 1
- colwidth: null
- align: null
```html
<td colspan="1" rowspan="1" colwidth="null/some-value" style="text-align: null/some-value;"><p class="paragraph-base"></p></td>
```
**callout**
Type: block
Content: block+ (one or more block child nodes (e.g. paragraph, list, …))
Callout / info box. The icon attr is an emoji shortcode (e.g. :information_source:, :warning:); it appears on the callout. Body is block content (paragraphs, lists, etc.) — use normal block HTML such as <p class="paragraph-base"> inside the callout <div>. data-type is "callout".
Atts:
- icon: :information_source:
```html
<div data-type="callout" icon=":information_source:"><p class="paragraph-base"></p></div>
```
**emoji**
Type: inline
Inline emoji. icon is a colon shortcode (e.g. :thumbsup:, :white_check_mark:) stored in data-icon; body is often empty. Use names your emoji set supports; avoid inventing invalid shortcodes in final HTML if you need them to render.
Atts:
- icon: :smile:
```html
<span data-type="emoji" data-icon=":smile:"></span>
```
### Marks
**link**
Atts:
- href: null
- target: _blank
- rel: noopener noreferrer nofollow
- class: null
- title: null
```html
<a target="_blank" rel="noopener noreferrer nofollow" href="">link text example</a>
```
**bold**
Atts: none
```html
<strong>bold text example</strong>
```
**code**
Atts: none
```html
<code>code text example</code>
```
**italic**
Atts: none
```html
<em>italic text example</em>
```
**strike**
Atts: none
```html
<s>strike text example</s>
```
**underline**
Atts: none
```html
<u>underline text example</u>
```
idstringVariant ID. Include this when updating an existing variant. Omit when creating a new variant.
namestringrequiredName of the variant (e.g., "Standard", "Pro", "Enterprise"). Used to distinguish different performance levels or configurations of the same product.
priceFixedobject | nullOne-time fixed price for this variant. Use this for one-time purchases (e.g., hardware, installation). Can be combined with subscription or per-unit pricing. Use 0 or null when the variant has no fixed charge.
pricePerUnitobject | nullPrice per unit for this variant. Use this for flexible billing by quantity (e.g., per user, per hour, per license). Requires priceUnitName to be set at the product level. Use 0 or null when the variant has no per-unit charge.
priceSubscriptionobject | nullRecurring subscription price for this variant. Use this for recurring revenue models (e.g., monthly SaaS subscription). The billing frequency is set at the product level via priceFrequency. Use 0 or null when the variant has no subscription charge.
Responses
200
categoryIdstringrequiredUUID of the product category this product belongs to
descriptionHtmlstringrequiredProduct description converted to HTML format for easy display
idstringrequiredUnique identifier of the product
logoIdobject | nullrequiredFile ID of the product logo. Null if no logo is set.
logoUrlobject | nullrequiredPublic URL to access the product logo image. Constructed from logoId if a logo is set. Null if no logo is available.
namestringrequiredName of the product or service
optionsstring[]requiredList of all product options with their values and configurations. Structure matches ProductOptionApiDto but with additional internal fields.
priceFixednumberrequiredOne-time fixed price for the product
priceFrequencyobject | nullrequiredBilling frequency in months (1, 3, 6, or 12). Null if subscription or per-unit pricing is not used.
pricePerUnitnumberrequiredPrice per unit for quantity-based billing
priceSubscriptionnumberrequiredRecurring subscription price for the product
priceUnitNameobject | nullrequiredName of the unit for per-unit pricing (e.g., "user", "hour"). Null if per-unit pricing is not used.
variantsIProductVariantApiResponse[]requiredList of all product variants with their pricing and descriptions
Show propertiesHide properties
Array of
IProductVariantApiResponsedescriptionHtmlobject | nullrequiredVariant description converted to HTML format. Null if no description is set.
idstringrequiredUnique identifier of the variant
isActivatedbooleanrequiredWhether this variant is currently activated and available for selection
namestringrequiredName of the variant (e.g., "Standard", "Pro", "Enterprise")
priceFixednumberrequiredOne-time fixed price for this variant
pricePerUnitnumberrequiredPrice per unit for this variant
priceSubscriptionnumberrequiredRecurring subscription price for this variant
400Validation errors
errorsobjectmessagestringstatusCodenumber401Unauthorized - Invalid or missing authentication token
403Forbidden - Insufficient API scopes or permissions
Request
curl -X POST "https://leadtime.app/api/public/administration/product-catalog" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"categoryId": "123e4567-e89b-12d3-a456-426614174000",
"contractPeriodMonths": 12,
"description": "string",
"logoId": "123e4567-e89b-12d3-a456-426614174000",
"name": "Website Hosting Package",
"options": [
{
"id": "string",
"isMultiple": false,
"isRequired": false,
"name": "string",
"values": [
{
"extraPriceFixed": 50,
"extraPricePerUnit": 2.5,
"extraPriceSubscription": 10,
"id": "string",
"name": "string"
}
]
}
],
"priceFixed": 999.99,
"priceFrequency": 1,
"pricePerUnit": 5,
"priceSubscription": 29.99,
"priceUnitName": "user",
"renewalMode": "Automatic",
"variants": [
{
"description": {},
"id": "string",
"name": "string",
"priceFixed": 999.99,
"pricePerUnit": 5,
"priceSubscription": 29.99
}
]
}'const response = await fetch("https://leadtime.app/api/public/administration/product-catalog", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
"categoryId": "123e4567-e89b-12d3-a456-426614174000",
"contractPeriodMonths": 12,
"description": "string",
"logoId": "123e4567-e89b-12d3-a456-426614174000",
"name": "Website Hosting Package",
"options": [
{
"id": "string",
"isMultiple": false,
"isRequired": false,
"name": "string",
"values": [
{
"extraPriceFixed": 50,
"extraPricePerUnit": 2.5,
"extraPriceSubscription": 10,
"id": "string",
"name": "string"
}
]
}
],
"priceFixed": 999.99,
"priceFrequency": 1,
"pricePerUnit": 5,
"priceSubscription": 29.99,
"priceUnitName": "user",
"renewalMode": "Automatic",
"variants": [
{
"description": {},
"id": "string",
"name": "string",
"priceFixed": 999.99,
"pricePerUnit": 5,
"priceSubscription": 29.99
}
]
})
});import requests
response = requests.post(
"https://leadtime.app/api/public/administration/product-catalog",
headers={
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json"
},
json={
"categoryId": "123e4567-e89b-12d3-a456-426614174000",
"contractPeriodMonths": 12,
"description": "string",
"logoId": "123e4567-e89b-12d3-a456-426614174000",
"name": "Website Hosting Package",
"options": [
{
"id": "string",
"isMultiple": False,
"isRequired": False,
"name": "string",
"values": [
{
"extraPriceFixed": 50,
"extraPricePerUnit": 2.5,
"extraPriceSubscription": 10,
"id": "string",
"name": "string"
}
]
}
],
"priceFixed": 999.99,
"priceFrequency": 1,
"pricePerUnit": 5,
"priceSubscription": 29.99,
"priceUnitName": "user",
"renewalMode": "Automatic",
"variants": [
{
"description": {},
"id": "string",
"name": "string",
"priceFixed": 999.99,
"pricePerUnit": 5,
"priceSubscription": 29.99
}
]
},
)Response
{
"categoryId": "string",
"descriptionHtml": "string",
"id": "string",
"logoId": {},
"logoUrl": {},
"name": "string",
"options": [
"string"
],
"priceFixed": 0,
"priceFrequency": {},
"pricePerUnit": 0,
"priceSubscription": 0,
"priceUnitName": {},
"variants": [
{
"descriptionHtml": {},
"id": "string",
"isActivated": true,
"name": "string",
"priceFixed": 0,
"pricePerUnit": 0,
"priceSubscription": 0
}
]
}{
"errors": {
"name": [
"Name is required"
],
"variants[0].priceFixed": [
"Price must be a positive number"
]
},
"message": "Bad Request",
"statusCode": 400
}Unauthorized - Invalid or missing authentication token
Forbidden - Insufficient API scopes or permissions