# Update product

`PATCH https://api.tentary.com/v1/products/{product_id}`

Source: https://tentary.com/en/help/api/update-product/
Area: Products and files
MCP tool: `update_product`. Changes data at once.

---

Changes the fields that you send. All other fields stay.

- `status` `live` publishes the product. This needs a finished account setup and content. `draft` hides it.
- `content` replaces all content blocks. Call [`GET /v1/products/{product_id}`](https://tentary.com/en/help/api/get-product/) first and send back the blocks that should stay, with their `file_id`.
- `null` removes an optional value: `compare_at_price`, `intro_price`, `limited_offer`, dates, limits, `url_slug` and `banner`.
- `course_id` changes the course that buyers of a course product get.

The fields have the same names and formats as in [`GET /v1/products/{product_id}`](https://tentary.com/en/help/api/get-product/).

## Authorization

`Authorization: Bearer <API key>`

A personal API key (tnt_...). Create it in the Tentary app on the page AI & API.

## Path parameters

- `product_id` (integer, required, path): The id from list_products. Limits: min 1.

## Request body

- `status` (string): live = buyers can see and buy it. draft = hidden. Allowed values: `draft`, `live`.
- `name` (string): Product name. Limits: minLength 1, maxLength 255.
- `price` (number | string, can be null): Amount in the account currency, like 49 or 19.99. 0 = free. Paid prices start at 1.00. For external: the price shown on the card. Limits: min 0, pattern ^[0-9]{1,9}(\.[0-9]{1,2})?$.
- `billing` (string): Default one_time. Allowed values: `one_time`, `subscription`.
- `billing_interval_months` (integer, can be null): subscription: months between payments, default 1. Limits: min 1, max 12.
- `intro_price` (number | string, can be null): subscription: price of the first period, less than price. Limits: min 0, pattern ^[0-9]{1,9}(\.[0-9]{1,2})?$.
- `installment_plans` (array of object): one_time: payment plans that buyers can choose. [] removes them. Limits: maxItems 5.
  - `count` (integer, required) Limits: min 2, max 100.
  - `amount` (number | string, required): Per installment. Amount in the account currency, like 49 or 19.99. Limits: pattern ^[0-9]{1,9}(\.[0-9]{1,2})?$.
- `compare_at_price` (number | string, can be null): one_time: crossed-out old price, higher than price. While a limited_offer runs, price is the crossed-out price instead; compare_at_price shows again when the offer ends. Limits: pattern ^[0-9]{1,9}(\.[0-9]{1,2})?$.
- `limited_offer` (object, can be null): one_time: a lower price for a time. While it runs, the sales page and the checkout show price crossed out and the offer price. Times with a zone; convert the seller's local time with timezone from get_account_overview.
  - `price` (number | string, required) Limits: pattern ^[0-9]{1,9}(\.[0-9]{1,2})?$.
  - `starts_at` (string, required): Like 2026-11-26T23:00:00Z.
  - `ends_at` (string, required): Like 2026-11-30T22:59:00Z.
  - `countdown` (object, can be null): A countdown to ends_at on the sales page and the checkout while the offer runs (made by JavaScript in the browser). null = none. Left out = unchanged. An object replaces the whole countdown.
    - `style` (string, required): classic and slim: a bar at the top (sticky: it stays while buyers scroll). card_*: a box next to the price. Allowed values: `classic`, `classic_sticky`, `slim`, `slim_sticky`, `card_classic`, `card_slim`, `card_special_1`.
    - `text` (string, can be null): Short text in the countdown, like "Launch price ends in". Limits: maxLength 255.
    - `color` (string): Background, hex like #ff4400 (default).
    - `secondary_color` (string, can be null): Bars: the text color (default white). Cards: the card background (default #1a1a1a).
- `description_html` (string): Sales page text as HTML (p, h1-h4, strong, em, ul, ol, li, a, img, blockquote; a with class "button" is a button). The page always has its own buy button. A buy button in the text links to the url of the product + "/checkout" (create_product and get_product give url); there is no #buy anchor. Limits: maxLength 200000.
- `checkout_description_html` (string): Extra text only on the checkout page, HTML. Not for external. Limits: maxLength 20000.
- `checkout_banner_url` (string, can be null): Banner image only on the checkout page, above the product title (it replaces the small product image). Recommended 1200 x 300 px. The image_url from finish_image, or a public image URL that Tentary copies. Not for external. Limits: maxLength 2048.
- `images` (array of string): Product images on the sales page as https URLs: the image_url from finish_image (for an image on your disk), or a public image URL that Tentary copies (PNG, JPG, GIF, WebP, at most 10 MB). The first is the cover. YouTube links show as video. Replaces all images. Limits: maxLength 2048, maxItems 20.
- `content` (array of object): download: what buyers get, in order. booking: what buyers get in addition to the appointment. Replaces all content. Limits: maxItems 100.
  - `type` (string, required): text: formatted text. image: shows the image. video: an uploaded video in the player (streamed, converted some minutes after its first use). audio: an audio player. file: a download. video_embed: a YouTube, Vimeo or Loom video. button: a link button. image, video, audio and file need file_id. Type, name and description are stored with the file: one file shows the same way in every product and lesson. Allowed values: `text`, `image`, `video`, `audio`, `file`, `video_embed`, `button`.
  - `html` (string): text: the text as HTML (p, h1-h4, strong, em, ul, ol, li, a, img, blockquote). Placeholders for the buyer: %FIRST_NAME%, %FULL_NAME%, %EMAIL%, %PRODUCT_TITLE%, %VARIANT_TITLE%, %ORDER_ID%, %ACCESS_LINK[Text]%, %ACCESS_LINK_BUTTON[Text]%, %SELLER_NAME%, %SELLER_SUPPORT_EMAIL%; bookings also %ORDER_BOOKING_FROM%, %ORDER_BOOKING_TO%, %ORDER_BOOKING_ZOOM_LINK%. Limits: maxLength 65535.
  - `url` (string): video_embed: YouTube, Vimeo or Loom link. button: the link target. Limits: maxLength 2048.
  - `label` (string): button: the button text. Limits: maxLength 250.
  - `file_id` (integer): image, video, audio, file: a file from upload_file, or a file_id from get_product or get_lesson content. Limits: min 1.
  - `name` (string): image, video, audio, file: the name of the file (file: shown above the download). Limits: minLength 1, maxLength 250.
  - `description` (string): file: plain text under the download. audio: the title above the player. Limits: maxLength 65535.
  - `folder` (string, can be null): Optional folder name. Blocks with the same folder show together. Not in lessons. Limits: maxLength 250.
- `link_url` (string, can be null): link: the link that buyers get. Limits: maxLength 2048.
- `access_text_html` (string, can be null): link: text that buyers see after purchase. bundle: extra details for the buyers. HTML. Takes the placeholders of text blocks. Limits: maxLength 65535.
- `bundle_items` (array of object, can be null): bundle: the products that buyers get, from list_products (live or draft; no bundles or bookings). For a product with variants, give the variant_id of one variant. Replaces the list. Needed to go live. Limits: maxItems 100.
  - `product_id` (integer, required) Limits: min 1.
  - `variant_id` (integer, can be null): Only for a product with variants. Limits: min 1.
- `external_url` (string, can be null): external: where buyers buy it. Limits: maxLength 2048.
- `course_id` (integer): course: the course that buyers get (from list_courses or create_course). Needed to go live. Limits: min 1.
- `variants` (array of object, can be null): download, link and course: options that buyers choose at checkout, each with its own name, price, stock and content (download: content, link: link_url and access_text_html, course: course_id). Replaces the list: a variant with id stays and changes (fields that you leave out stay), one without id is new, a variant that is not in the list is removed (its buyers keep access). [] or null removes all variants. With variants, price is the lowest variant price; billing must be one_time, without installment_plans, limited_offer and compare_at_price; a download product has no content of its own. Limits: maxItems 50.
  - `id` (integer): An existing variant (from get_product). Leave out for a new variant. Limits: min 1.
  - `name` (string): Needed for a new variant. Limits: minLength 1, maxLength 255.
  - `price` (number | string): Amount in the account currency, like 49 or 19.99. 0 = free. Needed for a new variant. Limits: min 0, pattern ^[0-9]{1,9}(\.[0-9]{1,2})?$.
  - `stock_limit` (integer, can be null): How many can be sold. null or 0 = no limit. Limits: min 0.
  - `content` (array of object): download: what buyers of this variant get. Replaces its content. Limits: maxItems 100.
    - `type` (string, required): text: formatted text. image: shows the image. video: an uploaded video in the player (streamed, converted some minutes after its first use). audio: an audio player. file: a download. video_embed: a YouTube, Vimeo or Loom video. button: a link button. image, video, audio and file need file_id. Type, name and description are stored with the file: one file shows the same way in every product and lesson. Allowed values: `text`, `image`, `video`, `audio`, `file`, `video_embed`, `button`.
    - `html` (string): text: the text as HTML (p, h1-h4, strong, em, ul, ol, li, a, img, blockquote). Placeholders for the buyer: %FIRST_NAME%, %FULL_NAME%, %EMAIL%, %PRODUCT_TITLE%, %VARIANT_TITLE%, %ORDER_ID%, %ACCESS_LINK[Text]%, %ACCESS_LINK_BUTTON[Text]%, %SELLER_NAME%, %SELLER_SUPPORT_EMAIL%; bookings also %ORDER_BOOKING_FROM%, %ORDER_BOOKING_TO%, %ORDER_BOOKING_ZOOM_LINK%. Limits: maxLength 65535.
    - `url` (string): video_embed: YouTube, Vimeo or Loom link. button: the link target. Limits: maxLength 2048.
    - `label` (string): button: the button text. Limits: maxLength 250.
    - `file_id` (integer): image, video, audio, file: a file from upload_file, or a file_id from get_product or get_lesson content. Limits: min 1.
    - `name` (string): image, video, audio, file: the name of the file (file: shown above the download). Limits: minLength 1, maxLength 250.
    - `description` (string): file: plain text under the download. audio: the title above the player. Limits: maxLength 65535.
    - `folder` (string, can be null): Optional folder name. Blocks with the same folder show together. Not in lessons. Limits: maxLength 250.
  - `link_url` (string, can be null): link: the link that buyers of this variant get. Limits: maxLength 2048.
  - `access_text_html` (string, can be null): link: text that buyers of this variant see after purchase, HTML. Takes the placeholders of text blocks. Limits: maxLength 65535.
  - `course_id` (integer): course: the course that buyers of this variant get. Needed for a new variant. Limits: min 1.
- `marketing_emails` (boolean): Subscribe buyers to the marketing e-mails (always on for newsletter).
- `ebook` (boolean): Treat as e-publication (e-book) for VAT.
- `pdf_stamp` (boolean): Stamp PDFs with the buyer's e-mail address.
- `login_protection` (boolean): Buyers must log in to open the product.
- `show_in_search` (boolean): Show the sales page in Google.
- `hide_in_shop` (boolean): Do not show the product on the shop page.
- `coupon_field` (boolean): Show the coupon field at checkout (default true).
- `stock_limit` (integer, can be null): How many can be sold. null or 0 = no limit. Limits: min 0.
- `buy_button_text` (string): Text of the buy button. Empty = default. Limits: maxLength 30.
- `url_slug` (string, can be null): Own URL part: shop URL/p/<url_slug>. Lowercase letters, digits and hyphens. Limits: maxLength 120.
- `access_days` (integer, can be null): Access ends this many days after purchase (3 to 9999). null or 0 = no end. Limits: min 0, max 9999.
- `access_starts_on` (string, can be null): Buyers get access from this day at the earliest (for example the start of a program). Before it they wait. null = at once. Not for newsletter and external. Limits: pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}$.
- `lead_magnet` (boolean): A free product that grows the e-mail list: buyers first confirm the marketing e-mails, then they get the product. Needs price 0 and billing one_time, and turns on marketing_emails. Not for newsletter, booking and external.
- `withdrawal_days` (integer): Withdrawal period shown at checkout. Allowed values: `0`, `14`, `30`.
- `available_from` (string, can be null): First day of sale. Limits: pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}$.
- `available_until` (string, can be null): Last day of sale. Limits: pattern ^[0-9]{4}-[0-9]{2}-[0-9]{2}$.

## Response

The short form. get_product has all fields.

- `id` (integer): The ID.
- `name` (string): The name.
- `type` (string): The type. Allowed values: `download`, `link`, `newsletter`, `booking`, `course`, `bundle`, `external`.
- `status` (string): archived = deleted. Allowed values: `live`, `draft`, `archived`.
- `url` (string<uri>): Sales page.
- `admin_url` (string<uri>): The page in the Tentary app.
- `note` (string): What to do next.

## Example request

```bash
curl -X PATCH "https://api.tentary.com/v1/products/105" \
  -H "Authorization: Bearer $TENTARY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "price": 19,
  "compare_at_price": 24,
  "show_in_search": true
}'
```

## Example response

```json
{
  "id": 105,
  "name": "Yoga Workbook",
  "type": "download",
  "status": "draft",
  "url": "https://lena-yoga.mytentary.com/p/d2hpTs",
  "admin_url": "https://app.tentary.com/products/edit/105",
  "note": "The product is a draft: buyers cannot see it. Set status live to publish it."
}
```
