Skip to main content

Products

The Products API lets you manage products in your store.

Overview​

The Products API provides endpoints to:

Host a file with Upload media. The same calls cover checkout and Creator-page files. Upload product media hosts a product file in a single multipart request.

Checkout and waitlist input definitions live under Custom Fields: list, get, create, update, and delete.

Checkout order bumps live under Order Bumps: list, get, create, update, delete, and reorder.

After-purchase cross-sells live under Cross-sells: list, get, create, update, delete, and reorder.

Checkout branding palettes live under Branding Themes. Attach a theme with branding_theme_id on Update product styles.

Unpublished overlay variants live under Product Variants: list, get, create, update, archive, and reorder.

Status and has_draft​

A product uses draft in two independent ways:

  • status is whether the product is on sale — draft or published.
  • has_draft is whether an unpublished overlay exists. Buyers still see the live product.

A published product can have has_draft: true. A product with status: draft can have has_draft: false.

Live list and get return the live product plus has_draft. They do not include draft_version, draft_updated_at, or preview_url. Get product draft returns the overlay when one exists, or the live product when it does not. draft_version is present on that response even when has_draft is false — send it on every draft write. preview_url is always present — see Draft preview.

Draft preview​

Get product draft and update product draft return preview_url. It is a temporary signed link to a checkout preview of the overlay — or of the live product when has_draft is false. It needs no Authorization header, so it can be opened straight in a browser. It expires about an hour after the read, so request the draft again for a fresh one instead of storing it. Once it has expired — or if the link is altered or its signature is dropped — the page returns 404.

Append ?lang=en or ?lang=pl to render the preview in that language. lang is the one parameter you can add without invalidating the link; any other value is ignored.

checkout_url is always the live checkout, not the overlay.

Create, update, archive, and reorder of a draft variant return a fresh product preview_url. Nested draft-variant list and get use the live variant shape and do not include preview_url. Draft custom-field create and update, draft order-bump create, update, and reorder, draft cross-sell create, update, and reorder, and draft styles, settings, and media writes, also return a fresh preview_url. Draft custom-field delete, order-bump delete, and cross-sell delete return 204 with no body.

Live writes while a draft exists​

When has_draft is true, these live writes return 409 and change nothing:

The body is a flat { "message": "…" } — the same envelope as other errors, with no error code. Edit the overlay instead (update product draft, nested draft-variant endpoints, nested /draft/order-bumps, nested /draft/cross-sells, and nested /draft/styles, /draft/settings, /draft/media). To drop the overlay, discard the draft. Product fields stored only on the draft are dropped. A course connect or disconnect stored by Update Product Draft is dropped with the draft and does not change the live product. Discarding does not roll draft_version back. Publish and unpublish persist the overlay as the live product and drop it. Only publish applies a stored course connect or disconnect — unpublish drops it. After a successful call, has_draft is false.

Upload media and upload product media are not blocked by has_draft — hosting a file does not change the gallery or a Creator page.

Promotion codes and other resources are not blocked by has_draft.

Active plan required​

Putting a product on sale requires the store to have an active Easytools plan. Without one, the store can still create, edit and keep drafts, but it cannot make anything live and cannot change anything that is already live.

These live writes are refused, each returning 422:

  • Create product with status: published — no product is created. Omitting status creates a draft. status: draft is always accepted.
  • Publish product — the product stays a draft. Publishing a product that is already published is refused in the same way.
  • Update product when the product is currently published, because the change would go live immediately. Updating a draft is always accepted.
  • Update product styles, settings, and media when the product is currently published. Overlay /draft/styles, /draft/settings, and /draft/media writes are always accepted.
  • Create product variant when the parent product is currently published. Adding a variant to a draft is always accepted.
  • Update product variant when the parent product is currently published. Updating a variant of a draft is always accepted.

A refused call creates and changes nothing, and returns a body with a message and no errors key:

{
"message": "Product cannot be published without an active Easytools plan"
}

These endpoints also return 422 for ordinary validation errors, with a field-keyed errors object next to the message. The presence of errors is what tells the two apart. The plan-required body is a flat { "message": "…" } with no errors key.

Repeating the request returns the same error until the store owner activates a plan; changing the request does not resolve it. Create products as drafts and publish them once the plan is active.

Nothing else is gated. Nested custom-field writes, upload media, and upload product media are not plan-gated. Unpublish product works regardless, so a live product can always be taken down; every read endpoint and every overlay write keeps working too. Live writes while an overlay exists return 409 — see Live writes while a draft exists.

Downloadable files​

A product can carry up to 5 files, delivered to the buyer after purchase. Each entry in files has a name, a url, and — on reads — a download_url.

url is the file's stored location, and the value to send back when writing files. For a file uploaded to your store it is not publicly readable: requesting it directly returns 403. For a file attached by link it is that URL exactly as it was provided.

To fetch a file's contents, follow its download_url. It is a temporary signed link that needs no Authorization header, so it can be opened straight in a browser. It expires about an hour after the read, so request the product again for a fresh one instead of storing it. Integrations that previously downloaded a file by fetching files[].url need to follow download_url instead. See Download Product File.

download_url appears only on reads. Write requests take name and url.

Gallery and logo hosting is a different write — Upload Media or Upload Product Media — and does not attach a downloadable file.

On get product draft and the nested draft-variant reads, a file that exists only on the overlay — attached by a draft write and not yet published — has download_url: null, because there is no stored copy to serve yet. It gets one once the draft is published. Files that were already live carry their download_url as usual.

A variant can carry its own files, which replace the product's for buyers of that variant. They use the same file object — see Product Variants.

URL fields​

Every URL a product or variant accepts — image_url, redirect_url, webhook_url, terms_url, policy_url, cta_url, contact_website_url, settings logo_url / logo_square_url / thank-you button_url / waitlist redirect_url, gallery file_url / thumbnail_url, and each file's url — must start with http:// or https:// and include a host. Almost nothing else is restricted: percent-encoding, query strings, fragments, explicit ports and non-ASCII characters in the path are all accepted, so a presigned storage link or a link with encoded spaces can be sent exactly as it is:

https://cdn.example.com/files/my%20file.pdf
https://bucket.s3.eu-central-1.amazonaws.com/f.pdf?X-Amz-Signature=abc&X-Amz-Expires=900
https://example.com/thanks#top
https://example.com:8443/thanks
https://example.com/a?tags=a,b

Anything else — a different scheme, a protocol-relative URL, a bare https:// with no host — is rejected with 422. Spaces and the characters <, > and " must be percent-encoded rather than sent raw.

The message is the same in every case, keyed by the field that failed:

{
"message": "The given data was invalid.",
"errors": {
"redirect_url": [
"Enter a valid link starting with http:// or https://"
]
}
}

Maximum lengths differ per field and are listed with each field on Create product, Update product, Update product variant, and Update product settings (logo_url / logo_square_url and thank-you / waitlist URLs). Gallery file_url / thumbnail_url are on Update product media.

Product images​

The image at image_url is the product's cover. It is downloaded and stored when a product is created or when image_url is sent on an update — including by update product draft, which re-hosts the image at the moment of the draft write rather than at publish. The image_url returned in responses, on the draft as well as the live product, points at that stored copy, so reading a product back returns a different URL than the one that was sent.

On update product and update product draft, sending image_url changes the cover only. Other images on the product are left unchanged. Send null to clear the cover only.

Gallery membership is a separate write — Update Product Media. Omitting the cover URL from gallery items removes that gallery row; image_url is not rewritten.

Checkout appearance​

Checkout colours, logos, thank-you copy, recovery, and the gallery are nested sections. They are not fields on create or update. Sending styles, settings, or media on those bodies is ignored.

Read them on Get Product and Get Product Draft with ?include=styles,settings,media. The keys are always present; the value is null unless that token is requested. When requested, styles and settings are objects even if nothing has been stored yet; media is a bare array (possibly empty) — not the { items, pagination } list envelope. List omits the keys. Create, update, publish, and unpublish responses leave the three keys null. Checkout and waitlist field definitions use the same include pattern — see Custom Fields.

Write them with:

Live section patches return 409 while has_draft is true. Hosting a file does not. Overlay writes use /products/{id}/draft/styles, /draft/settings, and /draft/media with required draft_version. There is no draft twin of the host upload. Unknown keys on a section patch return 422.

Cover stays image_url — see Product images.

Branding theme​

styles.branding_theme_id is the UUID of a store-owned branding theme, or null. It is always present when include=styles is requested.

Send branding_theme_id on Update Product Styles (live or /draft/styles):

BodyResult
{ "branding_theme_id": "f670a72a-7a46-4853-9ebb-91ddf691358e" }Attach. Copy each of the six theme colours (brand_color, background_color, text_color, card_background_color, card_text_color, border_color) onto the product only when that theme value is non-null. Copy logo_url / logo_square_url onto settings the same way. Existing product colours and logos stay when the matching theme field is null. main_background_color, main_border_color, price_text_color, and shadow tokens are not copied.
{ "branding_theme_id": null }Detach. Keep current colour tokens and logos.
A colour, border_radius, or shadow fieldDetach. Colour tokens change as sent.
Only darkmode_enabledLeave the theme attached.
Non-null branding_theme_id plus a colour, border_radius, or shadow field422 on branding_theme_id.
Unknown theme UUID422 on branding_theme_id.

theme_id is an unknown key (422). Create themes with Branding Themes first.

Colours​

Each colour field accepts null (clear), the literal transparent, or # plus 3–6 hex digits (#abc, #aabbcc). Values are stored and returned as sent — they are not expanded or lowercased. Anything else returns 422 on that field.

shadow_opacity is an integer 0–100. When this patch sets shadow_color to a hex value, shadow_opacity must be present in the same request (or already set after the merge). transparent or null shadow_color may send shadow_opacity: null. border_radius is an integer 0–100; 0 turns it off.

Sending a colour, border_radius, or a shadow field detaches any currently attached theme. Sending only darkmode_enabled leaves the theme attached. See Branding theme.

Settings​

show_logo controls whether the checkout logo is visible. Omit to leave it unchanged. When no settings have been stored yet, it reads as true.

Nested thank_you, waitlist_thank_you, and confirmation_email sparse-merge; send null on that object to clear its fields. Product footer_text, redirect_url, redirect_time, add_redirect_params, and show_confetti stay on the product — they are not duplicated under thank_you. Waitlist show_confetti / redirect_* live on waitlist_thank_you only.

checkout_recovery.enabled: null inherits the store default from Get store / Update store. coupon: null clears the coupon. amount and percent are mutually exclusive — 422 if both are set, or if the object has neither and is not null. amount is in minor units (e.g. cents). currency is present on GET when amount is set; sending it on write returns 422. The store coupon has no currency key.

Logos are URL-in (logo_url, logo_square_url). Host the file first with Upload Media or Upload Product Media, then send the returned file_url.

BodyError path
Unknown top-level or nested keythat path
Colour not matching the hex contractthat colour field
shadow_opacity out of 0–100, or missing while this patch sets a hex shadow_colorshadow_opacity
border_radius out of 0–100border_radius
checkout_recovery.coupon.amount and .percent both setcheckout_recovery.coupon
checkout_recovery.coupon object with neither amount nor percent (and not null)checkout_recovery.coupon
checkout_recovery.coupon.currency on writecheckout_recovery.coupon.currency
Thank-you or waitlist URL not HTTPthat field
Gallery items[].type not image or videoitems.{i}.type
Gallery items[].file_url missing or not an HTTP URLitems.{i}.file_url
Prepare content_type or size rejectedcontent_type or size on Upload Media
Upload Product Media bad type or missing filethat field
Non-null branding_theme_id plus a colour / border_radius / shadow keybranding_theme_id
Unknown branding_theme_idbranding_theme_id

Max lengths: thank_you.heading / button_text 250; thank_you.text 3000; thank_you.button_url 255; waitlist heading/button_text 250, text 3000, button_url 255, redirect_url 1000; confirmation_email.heading 255; confirmation_email.content 3000. Coupon percent 1–100. Coupon amount integer ≥ 0 minor units.

Styles Object​

Checkout appearance tokens. Present on reads when include=styles is requested; never null in that case.

FieldTypeNullableDescription
brand_colorstringYesBrand colour. See Colours.
main_background_colorstringYesPage background colour.
background_colorstringYesDescription-panel colour. Distinct from main_background_color.
text_colorstringYesBody text colour.
card_background_colorstringYesCard background colour.
card_text_colorstringYesCard text colour.
border_colorstringYesBorder colour.
main_border_colorstringYesMain border colour. transparent turns it off.
price_text_colorstringYesPrice text colour.
shadow_colorstringYesShadow colour. transparent turns it off.
border_radiusintegerYesCorner radius, 0–100.
shadow_opacityintegerYesShadow opacity, 0–100.
darkmode_enabledbooleanNoWhether dark mode is on. Defaults to false when no styles have been stored.
branding_theme_idstringYesStore-owned branding theme UUID, or null. See Branding theme.

Draft styles writes add draft_version and preview_url — see Draft preview.

Settings Object​

Checkout logos, thank-you pages, confirmation email, and recovery. Present on reads when include=settings is requested; never null in that case.

FieldTypeNullableDescription
logo_urlstringYesCheckout logo URL.
logo_square_urlstringYesSquare logo URL.
show_logobooleanNoWhether the checkout logo is visible. Defaults to true when no settings have been stored.
thank_youobjectNoPost-purchase thank-you. See Thank You Object.
waitlist_thank_youobjectNoWaitlist thank-you. See Waitlist Thank You Object.
confirmation_emailobjectNoConfirmation email. See Confirmation Email Object.
checkout_recoveryobjectNoAbandoned-checkout recovery. See Checkout Recovery Object.

Draft settings writes add draft_version and preview_url.

Thank You Object​

FieldTypeNullableDescription
headingstringYesHeading, max 250 characters.
textstringYesBody, max 3000 characters.
button_textstringYesButton label, max 250 characters.
button_urlstringYesButton URL, max 255 characters.

Waitlist Thank You Object​

FieldTypeNullableDescription
headingstringYesHeading, max 250 characters.
textstringYesBody, max 3000 characters.
show_confettibooleanNoConfetti on the waitlist thank-you page. Distinct from product show_confetti.
redirect_urlstringYesPost-waitlist redirect URL, max 1000 characters. Distinct from product redirect_url.
redirect_timeintegerYesRedirect delay in seconds.
add_redirect_paramsbooleanNoInclude parameters in the redirect URL. Defaults to true when no settings have been stored.
button_textstringYesButton label, max 250 characters.
button_urlstringYesButton URL, max 255 characters.

Confirmation Email Object​

FieldTypeNullableDescription
headingstringYesEmail heading, max 255 characters.
contentstringYesEmail body, max 3000 characters.

Checkout Recovery Object​

FieldTypeNullableDescription
enabledbooleanYesnull inherits the store default.
couponobjectYesDiscount offered in the recovery email. null means none. See Checkout Recovery Coupon Object.

Checkout Recovery Coupon Object​

amount and percent are mutually exclusive. amount is in minor units (e.g. cents) of the product currency.

FieldTypeNullableDescription
amountintegerYesFixed discount in minor units (e.g. cents). Mutually exclusive with percent.
percentintegerYesPercent discount, 1–100. Mutually exclusive with amount.
currencystringYesProduct currency (lowercase ISO code). Present on reads when amount is set. Not accepted on write.

When amount is set, a GET returns:

{
"amount": 500,
"percent": null,
"currency": "usd"
}

Media Item Object​

Gallery membership in display order. On GET with include=media, the value is a bare array of these items (possibly []). Cover remains image_url.

FieldTypeNullableDescription
file_urlstringNoGallery locator. Same key as Upload Media.
typestringNoimage or video. A host upload can also return file; that value is not accepted here.
thumbnail_urlstringYesThumbnail URL. On write, omit to keep the matching file_url's stored thumbnail; send JSON null to clear it.

Easytools attachments​

A Love wall, a product legal footer, and connected courses are read on Get Product and Get Product Draft with include=wall_id, include=footer_id, and include=player. Those tokens mix with custom_fields, waitlist_custom_fields, styles, settings, and media. Unknown tokens are ignored.

Without the token, the key is absent. null means the token was sent and nothing is attached. A failed lookup stays 200. That field alone is { "message": "The Easytools attachment could not be resolved." }. Other requested fields still return. One failed course or redirect UUID fails all of player, not one entry in player_ids.

List products does not accept these tokens. Create, update, publish, and unpublish responses omit the keys. Write them on Update Product and Update Product Draft. disconnect is not a read field.

A draft read can differ from the live product. See Get Product Draft.

TokenWhen omittedNothing attachedAttached
wall_idKey absentnullUUID string
footer_idKey absentnullUUID string
playerKey absentnullPlayer Object

A failed wall_id, footer_id, or player is the message object instead of a UUID or a player object.

Player Object​

Connected courses, returned only with include=player. This is not the write object on Update Product.

FieldTypeNullableDescription
redirect_idstringYesThank-you redirect UUID, or null when courses are connected and nothing redirects. When this is a UUID it is one of player_ids.
player_idsarrayNoConnected course UUIDs, in connection order, with duplicates dropped.

null for the whole player value means nothing is connected and there is no redirect.

A redirect plus another course:

{
"redirect_id": "a4e8c1d2-7b39-4f6a-9c05-1e2d8b4a6f70",
"player_ids": [
"a4e8c1d2-7b39-4f6a-9c05-1e2d8b4a6f70",
"c3b1e9f4-2a68-4d70-8e15-9f0a6c2d4b83"
]
}

Courses and no redirect:

{
"redirect_id": null,
"player_ids": [
"c3b1e9f4-2a68-4d70-8e15-9f0a6c2d4b83"
]
}

Checkout form​

A product's checkout can collect a delivery address, offer parcel delivery, show consent labels, and offer a discount with a newsletter opt-in. These fields appear on get, create, update, and the matching draft endpoints. They are not on the list.

A variant can store its own values for the shipping, consent, and newsletter fields — see Checkout overrides. Delegation fields exist only on the product.

has_shipping and delivery_enabled are separate:

  • has_shipping collects a delivery address.
  • delivery_enabled turns on parcel delivery. That requires a delivery_service carrier slug and a complete Delivery Options Object.

delivery_service is one of dpd, fedex, ups, gls, poczta, inpost, inpostkurier, orlen, dhl, meest, ambroexpress, furgonetka, deligoo, xpressdelivery, pallex, swiatprzesylek.

Consent fields are labels. A non-empty string shows that consent; null turns it off. delegation_label and phone_tooltip are at most 250 characters; consent labels are at most 500.

On create, omitted booleans default to false except delivery_address_optional, which defaults to true. Omitted labels, delivery_service, delivery_options, and newsletter_discount default to null. On update, omit a field to leave it unchanged. Send null on newsletter_discount to clear that section. Send null on delivery_options to delete the stored package only when the resulting delivery_enabled is false (already false, or send delivery_enabled: false in the same request). Sending delivery_options: null while delivery_enabled stays true returns 422. A sent delivery_options object replaces the stored package in full — omitted keys inside it become null.

newsletter_discount.currency is read-only (the product's currency, present when amount is set). Sending currency on create or update returns 422.

BodyError path
delivery_service is not a known carrier slugdelivery_service
delivery_enabled: true without delivery_servicedelivery_service
Resulting delivery_enabled is true without a complete delivery_options (all five keys non-null), including delivery_options: null while delivery stays ondelivery_options
width_cm / length_cm / height_cm below 1, or weight_kg below 0.1that nested path
newsletter_discount.amount and newsletter_discount.percent both setnewsletter_discount
duration: repeating without duration_monthsnewsletter_discount.duration_months
amount set with duration: forevernewsletter_discount.duration
newsletter_discount.currency on create or updatenewsletter_discount.currency

Invoice, tax, portal, waitlist, and QR​

A product can store invoice remarks, tax settings, self-service portal copy, extra contact details, checkout footer copy, waitlist flags, and QR credits. These fields appear on get, create, update, and the matching draft endpoints. They are not on the list.

A variant can store its own tax, portal, and credits values — plus pay-what-you-want and show_net_price. Reads return what is stored, not the checkout combination. See Checkout overrides. Waitlist flags, contact extras, footer_text, qr_code_info, and reset_credits exist only on the product.

A product vat_rate, flat_rate, gtu_code, or exempt_tax_kind that is null inherits the matching store default at checkout — see Tax defaults.

vat_rate is a string — "23", "zw", or "np" — not a number. gtu_code is an integer from 1 to 13 as stored, not a GTU_01 label. flat_rate is one of 17, 15, 14, 12.5, 12, 10, 8.5, 5.5, 3, 2. tax_category is one of saas, ebook, course, webinar, digital, consulting, physical, exempt. It is distinct from a variant's tax_behavior.

Portal copy (dashboard_header, long_description, cta_text, cta_url) is the self-service portal, not the product description. Product long_description is at most 25,000 characters; a variant's is at most 500. cta_url and contact_website_url follow URL fields. contact_website_url is this product's contact site — distinct from the store website_url.

credits: null means credits are off, not zero. The maximum is 4294967295; a larger value returns 422. Omitting footer_text on create persists as an empty string (""), not JSON null.

On create, omitted strings, tax values, and credits default to null (footer_text to ""). Omitted waitlist flags and reset_credits default to false. On update, omit a field to leave it unchanged. Send null on a nullable string, tax value, or credits to clear it.

BodyError path
vat_rate is not a valid ratevat_rate
gtu_code is not an integer from 1 to 13gtu_code
flat_rate is not in the allowed setflat_rate
tax_category is not a known slugtax_category
cta_url / contact_website_url is not an HTTP URLthat field
credits is negative or greater than 4294967295credits

An invalid vat_rate uses the same 422 envelope. The message on that field may be in Polish:

{
"message": "The given data was invalid.",
"errors": {
"vat_rate": [
"Niepoprawna stawka VAT"
]
}
}

Delivery Options Object​

Parcel package for delivery_enabled. Present on reads when a package is stored; null otherwise.

FieldTypeNullableDescription
width_cmintegerYesPackage width in centimetres. Minimum 1.
length_cmintegerYesPackage length in centimetres. Minimum 1.
height_cmintegerYesPackage height in centimetres. Minimum 1.
weight_kgnumberYesPackage weight in kilograms. Minimum 0.1.
contentsstringYesPackage contents description (max 255 characters).

Newsletter Discount Object​

Discount offered when the buyer opts into the newsletter. amount and percent are mutually exclusive. amount is in minor units (e.g. cents) of the product currency.

FieldTypeNullableDescription
amountintegerYesFixed discount in minor units (e.g. cents). Mutually exclusive with percent.
percentintegerYesPercent discount, 1–100. Mutually exclusive with amount.
durationstringNoHow long the discount applies: once, repeating, or forever. forever cannot be combined with amount.
duration_monthsintegerYesMonths the discount repeats. Required when duration is repeating. Minimum 1.
currencystringYesProduct currency (lowercase ISO code). Present on reads when amount is set. Not accepted on create or update.