Update Product Variant
Update an existing product variant in your store. This endpoint uses PATCH semantics, meaning only the fields you include in the request body will be updated.
Updating a variant of a product that is currently published requires an active Easytools
plan, because the change goes live immediately. If the store has none, the call returns
422 and nothing is changed. Updating a variant of a draft product is always accepted.
See Active plan required.
When the product has has_draft: true, the call returns 409 and nothing is changed.
Edit the overlay with Update Product Draft Variant,
or discard the draft first. See
Live writes while a draft exists.
amount, type, currency, tax_behavior, and recurring_options cannot be changed
after create. Sending any of them returns 422 and nothing is changed. See
Immutable price fields.
Sending custom_fields here is ignored. Use the nested
Custom Fields endpoints.
Request
PATCH /product-variants/{id}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Product variant UUID |
Request Body
All fields are optional. Only include the fields you want to update.
| Parameter | Type | Description |
|---|---|---|
name | string | Variant display name (max 255 characters). Set to null to clear. |
custom_id | string | External identifier for integration with other systems (max 255 characters). Set to null to clear. |
is_hidden | boolean | Hide variant from checkout page |
trial_period_days | integer | Trial period in days before first charge (recurring variants only, min 0). Set to null to disable. |
active_cycles | integer | Number of billing cycles for payment plans (min 1). Set to null for unlimited. |
cancel_early | boolean | Allow customers to cancel subscription before active_cycles complete |
keep_access_after_expiring | boolean | Keep access to content after subscription ends |
allow_multiple_subscriptions | boolean | Allow customers to have multiple concurrent subscriptions |
active_days | integer | Access duration in days after purchase, relative-expiry mode (min 1). Set to null for unlimited. |
access_until | string | Fixed access-expiry date (ISO 8601), fixed-expiry mode. The variant stops selling on this date at the latest — see Access expiry modes. Set to null to clear. |
quantity | integer | Available inventory limit (min 0). Set to null for unlimited. |
has_quantity | boolean | Enable quantity selector on checkout page |
old_price | integer | Struck-through compare-at price in major currency units (79 = 79.00 PLN). Min: 0. Unlike amount, which is in minor units (e.g. cents). Set to null to clear. See Price units. |
active_from | string | Start selling date (ISO 8601 format). Set to null to clear. |
active_until | string | Stop selling date (ISO 8601 format). Set to null to clear. With access_until set, it can't be later than access_until, and null resets it to access_until. |
show_active_until_counter | boolean | Show countdown timer on checkout page |
refund_days | integer | Refund period in days, overrides product setting (min 0). Set to null to use product default. |
redirect_url | string | Post-purchase redirect URL, overrides product setting (max 1000 characters). Set to null to use product default. |
redirect_time | integer | Redirect delay in seconds (min 0). Set to null to use product default. |
file | object | Single variant-specific downloadable file, overrides product files. Legacy shortcut equivalent to files: [file]. Set to null to remove. See File Object. |
files | array | Up to 5 variant-specific downloadable files, overriding the product's files. Each entry is a File Object. When provided, this supersedes file and replaces the variant's entire file set. Send [] to remove all variant files (falls back to the product's files). |
invoice_remarks | string | Additional notes to include on invoices (max 250 characters). Set to null to clear. |
has_shipping | boolean | Collect a delivery address for this variant. Checkout ORs this with the product — see Checkout overrides. |
delivery_enabled | boolean | Enable parcel delivery on this variant. When true, delivery_service and a complete delivery_options object are required. |
delivery_service | string | Carrier slug. See Checkout form. Set to null to clear the override only when the resulting delivery_enabled on this variant is false. |
delivery_address_optional | boolean | Whether the delivery address is optional on this variant |
delivery_options | object | Parcel package. See Delivery Options Object. Replaces the stored package in full. Set to null to clear the override when the resulting delivery_enabled on this variant is false. Returns 422 while this variant still has delivery on. |
shipping_phone_required | boolean | Require a phone number with the delivery address |
phone_tooltip | string | Helper text for the shipping phone field (max 250 characters). Set to null to inherit the product. |
mandatory_consent | string | Mandatory checkout consent label (max 500 characters). Set to null to inherit the product. |
optional_consent | string | Optional checkout consent label (max 500 characters). Set to null to inherit the product. |
digital_consent | string | Digital-content consent label (max 500 characters). Set to null to inherit the product. |
newsletter_consent | string | Newsletter opt-in label (max 500 characters). Set to null to inherit the product. |
newsletter_consent_required | boolean | Whether the newsletter opt-in is required on this variant |
newsletter_discount | object | Discount offered with newsletter opt-in. See Newsletter Discount Object. Do not send currency. Set to null to inherit the product. |
vat_rate | string | VAT rate as a string (23, zw, np). Set to null to inherit the product (not 0%). See Checkout overrides. |
gtu_code | integer | GTU code 1–13 as stored. Set to null to inherit the product. |
flat_rate | number | Flat-rate tax percent from the allowed set. Set to null to inherit the product. |
exempt_tax_kind | string | Exemption basis when VAT is zw. Set to null to inherit the product. |
tax_category | string | Tax category slug. Distinct from tax_behavior. Set to null to inherit the product. |
dashboard_header | string | Self-service portal header (max 250 characters). Set to null to inherit the product. |
long_description | string | Self-service portal body (max 500 characters). Set to null to inherit the product. |
cta_text | string | Self-service portal call-to-action label (max 50 characters). Set to null to inherit the product. |
cta_url | string | Self-service portal call-to-action URL (max 255 characters). Set to null to inherit the product. |
credits | integer | QR credits stored on this variant. Set to null to inherit the product, not zero. Minimum 0, maximum 4294967295 (422 if exceeded). |
show_net_price | boolean | Whether checkout shows the net price. |
pay_what_you_want | object | Pay-what-you-want settings. See Pay What You Want Object. Set to null to turn it off (does not clear purchase_button_text). Cannot be enabled on a recurring variant. |
purchase_button_text | string | Checkout purchase-button label (pay-what-you-want and lead magnet). Sibling of pay_what_you_want. Set to null to clear. |
upsells | array | Same-product checkout upgrades. Replaces the list in full. Omit to leave unchanged; send [] to clear. See Upsells. |
redirect_url, cta_url, and each file's url must start with http:// or https://
and include a host. See URL fields for what is accepted and
what returns 422.
File Object
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name shown in the customer portal (max 100 characters) |
url | string | Yes | The file's location (max 2048 characters): a URL to attach the file by link, or the url of a file read back from the API |
Reads add a read-only third property, download_url — see
Response Fields. Requests take name and url
only, so a file kept from an earlier read is sent back without it.
A variant can have up to 5 downloadable files. Variant files override the product's files: when a variant has any files, customers see only the variant's files; when it has none, they fall back to the product's files. Updating files (or the legacy file) replaces the variant's entire set.
Example Requests
Update variant name and custom ID:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Premium Package",
"custom_id": "SKU-PREMIUM-001"
}'
Clear this variant's newsletter discount so checkout uses the product's:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"newsletter_discount": null
}'
Enable pay-what-you-want on a one-time variant:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pay_what_you_want": {
"enabled": true,
"suggested_amount": 4900,
"minimum_amount": 1000
},
"purchase_button_text": "Pay yours"
}'
Turn pay-what-you-want off. purchase_button_text is left unchanged:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pay_what_you_want": null
}'
Attach same-product upsells. The listed variant_id must be a published sibling of
this product. Omit upsells to leave the list unchanged:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"upsells": [
{
"variant_id": "e903c6d6-163e-4080-a99a-4ee5a1e4f17a",
"text": "Switch to a monthly subscription"
}
]
}'
Clear every upsell:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"upsells": []
}'
Update availability settings with sale pricing:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"active_from": "2026-02-01T00:00:00Z",
"active_until": "2026-03-01T00:00:00Z",
"show_active_until_counter": true,
"old_price": 29
}'
Update subscription settings:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/e903c6d6-163e-4080-a99a-4ee5a1e4f17a" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"trial_period_days": 14,
"active_cycles": 12,
"cancel_early": true,
"keep_access_after_expiring": false
}'
Update file attachment:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"file": {
"name": "Premium Ebook.pdf",
"url": "https://mysite.com/files/premium-ebook.pdf"
}
}'
Attach multiple variant-specific files:
curl -X PATCH "https://cart.easy.tools/api/v1/product-variants/d91c65aa-b221-4aa4-b626-cf0d8a198749" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"files": [
{ "name": "Premium Ebook.pdf", "url": "https://mysite.com/files/premium-ebook.pdf" },
{ "name": "Workbook.pdf", "url": "https://mysite.com/files/workbook.pdf" }
]
}'
Response
Success Response (200)
Returns the updated product variant with all fields.
{
"id": "d91c65aa-b221-4aa4-b626-cf0d8a198749",
"status": "published",
"type": "one_time",
"recurring_options": null,
"name": "Premium Package",
"amount": 1999,
"currency": "usd",
"checkout_url": "https://cart.easy.tools/checkout/premium-coffee?plan=price_123",
"custom_id": "SKU-PREMIUM-001",
"is_hidden": false,
"trial_period_days": null,
"active_cycles": null,
"cancel_early": false,
"keep_access_after_expiring": false,
"allow_multiple_subscriptions": false,
"active_days": null,
"quantity": null,
"has_quantity": false,
"old_price": 29,
"active_from": "2026-02-01T00:00:00+00:00",
"active_until": "2026-03-01T00:00:00+00:00",
"access_until": null,
"show_active_until_counter": true,
"tax_behavior": "exclusive",
"refund_days": null,
"redirect_url": null,
"redirect_time": null,
"file": {
"name": "Premium Ebook.pdf",
"url": "https://mysite.com/files/premium-ebook.pdf",
"download_url": "https://cart.easy.tools/api/v1/product-files/6a3f9d24-0b71-4c85-9e6a-1d8b2f45c703/download?expires=1749384000&signature=abc123"
},
"files": [
{
"name": "Premium Ebook.pdf",
"url": "https://mysite.com/files/premium-ebook.pdf",
"download_url": "https://cart.easy.tools/api/v1/product-files/6a3f9d24-0b71-4c85-9e6a-1d8b2f45c703/download?expires=1749384000&signature=abc123"
},
{
"name": "Workbook.pdf",
"url": "https://mysite.com/files/workbook.pdf",
"download_url": "https://cart.easy.tools/api/v1/product-files/d1c8b573-4e29-4a06-8f7b-52e3a9d61c48/download?expires=1749384000&signature=abc123"
}
],
"invoice_remarks": null,
"has_shipping": false,
"delivery_enabled": false,
"delivery_service": null,
"delivery_address_optional": true,
"delivery_options": null,
"shipping_phone_required": false,
"phone_tooltip": null,
"mandatory_consent": null,
"optional_consent": null,
"digital_consent": null,
"newsletter_consent": null,
"newsletter_consent_required": false,
"newsletter_discount": null,
"vat_rate": null,
"gtu_code": null,
"flat_rate": null,
"exempt_tax_kind": null,
"tax_category": null,
"dashboard_header": null,
"long_description": null,
"cta_text": null,
"cta_url": null,
"credits": null,
"show_net_price": false,
"pay_what_you_want": null,
"purchase_button_text": null,
"upsells": [],
"custom_fields": null
}
{
"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",
"custom_id": null,
"is_hidden": false,
"trial_period_days": 14,
"active_cycles": 12,
"cancel_early": true,
"keep_access_after_expiring": false,
"allow_multiple_subscriptions": false,
"active_days": null,
"quantity": null,
"has_quantity": false,
"old_price": null,
"active_from": null,
"active_until": null,
"access_until": null,
"show_active_until_counter": null,
"tax_behavior": "exclusive",
"refund_days": null,
"redirect_url": null,
"redirect_time": null,
"file": null,
"files": [],
"invoice_remarks": null,
"has_shipping": false,
"delivery_enabled": false,
"delivery_service": null,
"delivery_address_optional": true,
"delivery_options": null,
"shipping_phone_required": false,
"phone_tooltip": null,
"mandatory_consent": null,
"optional_consent": null,
"digital_consent": null,
"newsletter_consent": null,
"newsletter_consent_required": false,
"newsletter_discount": null,
"vat_rate": null,
"gtu_code": null,
"flat_rate": null,
"exempt_tax_kind": null,
"tax_category": null,
"dashboard_header": null,
"long_description": null,
"cta_text": null,
"cta_url": null,
"credits": null,
"show_net_price": false,
"pay_what_you_want": null,
"purchase_button_text": null,
"upsells": [],
"custom_fields": null
}
Response Fields
See Get Product Variant for a complete list of response fields.
Error Responses
Bad Request (400)
{
"message": "Invalid product variant ID"
}
Not Found Error (404)
{
"message": "Product variant with ID e1b6d3f8-7c2a-4f9e-b4d1-6a8c3e7b2f50 not found"
}
Conflict Error (409 — Unpublished Draft)
Returned when an unpublished overlay exists. Nothing is changed. Edit 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)
Returned when an immutable price field is sent — see Immutable price fields — or when another field fails validation. Sending an immutable field changes nothing, including other fields in the same body.
{
"message": "The amount field cannot be updated. Stripe does not allow changing the amount on a published price.",
"errors": {
"amount": [
"The amount field cannot be updated. Stripe does not allow changing the amount on a published price."
]
}
}
{
"message": "The given data was invalid.",
"errors": {
"active_until": [
"The active until field must be a date after or equal to active from."
]
}
}
{
"message": "The given data was invalid.",
"errors": {
"redirect_url": [
"Enter a valid link starting with http:// or https://"
],
"file.url": [
"Enter a valid link starting with http:// or https://"
]
}
}
Sellability fields use the same envelope. See
Checkout form and
Checkout overrides. Pay-what-you-want and tax fields
use it too — see Pay What You Want Object.
upsells uses it too — see Upsells:
{
"message": "The given data was invalid.",
"errors": {
"upsells.0.variant_id": [
"A variant cannot upsell to itself."
]
}
}
Publishing Not Allowed (422 — No Active Plan)
Returned when the variant's product is currently published and the store has no active Easytools plan. Nothing is changed. Activate a plan and retry — repeating the call as it stands returns the same error. Variants of a draft product 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"
}
Notes
- Immutable fields:
amount,type,currency,tax_behavior, andrecurring_optionscannot be changed after create. Sending any of them returns422— see Immutable price fields. - Access expiry:
active_days(relative — N days after purchase) andaccess_until(fixed calendar date) are the two access-expiry modes. If both are set,access_untiltakes precedence at fulfilment. For howaccess_untillimitsactive_until, see Access expiry modes.