Skip to main content

Create Product Variant

Create a new product variant for a product in your store.

Adding a variant to a product that is currently published requires an active Easytools plan, because the variant goes live immediately. If the store has none, the call returns 422 and no variant is created. Adding a variant to a draft product is always accepted. See Active plan required.

When the product has has_draft: true, the call returns 409 and no variant is created. Create the variant on the overlay with Create Product Draft Variant, or discard the draft first. See Live writes while a draft exists.

Request​

POST /products/{id}/variants

Path Parameters​

ParameterTypeRequiredDescription
idstringYesProduct UUID

Request Body​

ParameterTypeRequiredDescription
typestringYesVariant type ("one_time" or "recurring")
namestringNoVariant name (max 255 characters)
recurring_optionsobjectNoRecurring options (required if type is "recurring")
recurring_options.intervalstringNoRecurring interval ("day", "week", "month", "year")
recurring_options.interval_countintegerNoNumber of intervals between charges
amountintegerYesPrice in minor units (minimum 0)
trial_period_daysintegerNoTrial period in days (min: 0, recurring variants only)
active_cyclesintegerNoNumber of billing cycles (min: 1, recurring only)
is_hiddenbooleanNoHide variant from checkout (default: false)
quantityintegerNoInventory limit (min: 0, null = unlimited)
has_quantitybooleanNoEnable quantity selector (default: false)
old_priceintegerNoStruck-through compare-at price in major currency units (79 = 79.00 PLN). Min: 0. Unlike amount, which is in minor units (e.g. cents). See Price units.
active_fromstringNoStart selling date (ISO 8601)
active_untilstringNoEnd selling date (ISO 8601, must be >= active_from). With access_until set, defaults to access_until and can't be later — see Access expiry modes
active_daysintegerNoDays of access after purchase (relative-expiry mode, min: 1)
access_untilstringNoFixed access-expiry date (ISO 8601). The variant stops selling on this date at the latest — see Access expiry modes
tax_behaviorstringNoTax handling ("inclusive" or "exclusive")
has_shippingbooleanNoCollect a delivery address for this variant. Defaults to false. Checkout ORs this with the product — see Checkout overrides.
delivery_enabledbooleanNoEnable parcel delivery on this variant. Defaults to false. When true, delivery_service and a complete delivery_options object are required.
delivery_servicestringNoCarrier slug. See Checkout form. Required when delivery_enabled is true.
delivery_address_optionalbooleanNoWhether the delivery address is optional. Defaults to true.
delivery_optionsobjectNoParcel package. See Delivery Options Object. Required in full when delivery_enabled is true.
shipping_phone_requiredbooleanNoRequire a phone number with the delivery address. Defaults to false.
phone_tooltipstringNoHelper text for the shipping phone field (max 250 characters).
mandatory_consentstringNoMandatory checkout consent label (max 500 characters). Omit or null inherits the product.
optional_consentstringNoOptional checkout consent label (max 500 characters).
digital_consentstringNoDigital-content consent label (max 500 characters).
newsletter_consentstringNoNewsletter opt-in label (max 500 characters).
newsletter_consent_requiredbooleanNoWhether the newsletter opt-in is required on this variant. Defaults to false.
newsletter_discountobjectNoDiscount offered with newsletter opt-in. See Newsletter Discount Object. Do not send currency.
vat_ratestringNoVAT rate as a string (23, zw, np). Omit stores null (inherit the product at checkout, not 0%). See Checkout overrides.
gtu_codeintegerNoGTU code 1–13 as stored, not a GTU_01 label. Omit stores null.
flat_ratenumberNoFlat-rate tax percent: 17, 15, 14, 12.5, 12, 10, 8.5, 5.5, 3, or 2. Omit stores null.
exempt_tax_kindstringNoExemption basis when VAT is zw (max 255 characters).
tax_categorystringNoTax category: saas, ebook, course, webinar, digital, consulting, physical, or exempt. Distinct from tax_behavior.
dashboard_headerstringNoSelf-service portal header (max 250 characters). Empty or omit inherits the product.
long_descriptionstringNoSelf-service portal body (max 500 characters on a variant). Distinct from product description.
cta_textstringNoSelf-service portal call-to-action label (max 50 characters).
cta_urlstringNoSelf-service portal call-to-action URL (max 255 characters).
creditsintegerNoQR credits stored on this variant. Omit or null inherits the product, not zero. Minimum 0, maximum 4294967295 (422 if exceeded).
show_net_pricebooleanNoWhether checkout shows the net price. Defaults to false.
pay_what_you_wantobjectNoPay-what-you-want settings. See Pay What You Want Object. Omit stores null (off). Cannot be enabled on a recurring variant.
purchase_button_textstringNoCheckout purchase-button label (pay-what-you-want and lead magnet). Sibling of pay_what_you_want, not nested (max 25 characters).

upsells is not accepted on create. The response still includes upsells: []. Attach upsells with Update Product Variant after the target siblings exist. See Upsells.

Sending custom_fields here is ignored. Set the checkout override with the nested Custom Fields endpoints after the variant exists.

cta_url must start with http:// or https:// and include a host. See URL fields.

Example Request​

curl -X POST "https://cart.easy.tools/api/v1/products/0632bef5-c308-42cd-9cba-89a7a4f722bb/variants" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "one_time",
"name": "250g Package",
"amount": 1999,
"old_price": 29,
"quantity": 100,
"has_quantity": true
}'
curl -X POST "https://cart.easy.tools/api/v1/products/0632bef5-c308-42cd-9cba-89a7a4f722bb/variants" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "recurring",
"name": "Monthly Subscription - 250g",
"recurring_options": {
"interval": "month",
"interval_count": 1
},
"amount": 1799,
"trial_period_days": 14,
"active_cycles": 12,
"tax_behavior": "inclusive"
}'

Override shipping on this variant (stored true; omit the flag to store false):

curl -X POST "https://cart.easy.tools/api/v1/products/0632bef5-c308-42cd-9cba-89a7a4f722bb/variants" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "one_time",
"name": "250g Package",
"amount": 1999,
"has_shipping": true
}'

Pay-what-you-want on a one-time variant. Amounts are minor units of the product currency:

curl -X POST "https://cart.easy.tools/api/v1/products/0632bef5-c308-42cd-9cba-89a7a4f722bb/variants" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "one_time",
"name": "Pay what you want",
"amount": 0,
"pay_what_you_want": {
"enabled": true,
"suggested_amount": 4900,
"minimum_amount": 1000
}
}'

Response​

Success Response (201)​

Returns the created product variant.

{
"id": "d91c65aa-b221-4aa4-b626-cf0d8a198749",
"status": "published",
"type": "one_time",
"recurring_options": null,
"name": "250g Package",
"amount": 1999,
"currency": "usd",
"checkout_url": "https://cart.easy.tools/checkout/premium-coffee?plan=price_123"
}
{
"id": "e903c6d6-163e-4080-a99a-4ee5a1e4f17a",
"status": "published",
"type": "recurring",
"recurring_options": {
"interval": "month",
"interval_count": 1
},
"name": "Monthly Subscription - 250g",
"amount": 1799,
"currency": "usd",
"checkout_url": "https://cart.easy.tools/checkout/premium-coffee?plan=price_456"
}

Response Fields​

FieldTypeNullableDescription
idstringNoUnique identifier for the product variant (UUID)
statusstringNoVariant lifecycle (published or archived). See Status.
typestringNoVariant type ("one_time" or "recurring")
recurring_optionsobjectYesRecurring options (null for one-time variants)
recurring_options.intervalstringNoRecurring interval ("day", "week", "month", "year")
recurring_options.interval_countintegerNoNumber of intervals between charges
namestringYesVariant name
amountintegerNoPrice in minor units
currencystringNoLowercase ISO currency code (inherited from the product)
checkout_urlstringYesDirect checkout URL for this variant. Null until the variant is synced to Stripe.

The response contains the full variant. See Get Product Variant for the complete list of fields; the examples above are abbreviated.

Error Responses​

Not Found Error (404)​

Returned when the UUID does not exist, belongs to another store, or points at an archived product. Variants can only be added to draft and published products.

{
"message": "Product with ID 0632bef5-c308-42cd-9cba-89a7a4f722bb not found"
}

Conflict Error (409 — Unpublished Draft)​

Returned when an unpublished overlay exists. No variant is created. Create it on the overlay, or discard the draft first.

{
"message": "This product has an unpublished draft. Publish, unpublish, or delete the draft before making live changes."
}

Validation Error (422 Unprocessable Entity)​

{
"message": "The given data was invalid.",
"errors": {
"type": [
"This field is required."
],
"recurring_options": [
"The recurring options field is required when type is recurring."
],
"amount": [
"This field is required."
]
}
}

Sellability fields use the same envelope. See Checkout form and Checkout overrides. Pay-what-you-want and tax fields use it too — a recurring variant cannot enable pay-what-you-want:

{
"message": "The given data was invalid.",
"errors": {
"pay_what_you_want": [
"Pay-what-you-want is not available on recurring variants."
]
}
}

Publishing Not Allowed (422 — No Active Plan)​

Returned when the parent product is currently published and the store has no active Easytools plan. No variant is created. Activate a plan and retry — repeating the call as it stands returns the same error. Draft products are unaffected.

This body carries a message and no errors key, so a 422 from this endpoint is distinguished by whether errors is present.

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