> ## Documentation Index
> Fetch the complete documentation index at: https://docs.whop.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Variant

A Variant is one purchasable configuration of a product. It controls price, billing cadence, stock, SKU, attributes, tax behavior, checkout fields, and purchase visibility. Variant IDs remain prefixed `plan_`.

Use the Variants API to create variants for products, list existing variants, retrieve or update variant configuration, calculate tax for checkout, and delete variants that should no longer be offered.

## Endpoints

| Endpoint                                                          | Request                                                                           |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| [List Variants](/api-reference/beta/variants/list-variants)       | <Badge color="blue" size="sm" stroke>GET</Badge> `/variants`                      |
| [Retrieve Variant](/api-reference/beta/variants/retrieve-variant) | <Badge color="blue" size="sm" stroke>GET</Badge> `/variants/{id}`                 |
| [Create Variant](/api-reference/beta/variants/create-variant)     | <Badge color="green" size="sm" stroke>POST</Badge> `/variants`                    |
| [Calculate Tax](/api-reference/beta/variants/calculate-tax)       | <Badge color="green" size="sm" stroke>POST</Badge> `/variants/{id}/calculate_tax` |
| [Update Variant](/api-reference/beta/variants/update-variant)     | <Badge color="orange" size="sm" stroke>PATCH</Badge> `/variants/{id}`             |
| [Delete Variant](/api-reference/beta/variants/delete-variant)     | <Badge color="red" size="sm" stroke>DELETE</Badge> `/variants/{id}`               |

## Attributes

<Columns cols={2}>
  <Column>
    <ResponseField name="id" type="string" required>
      Variant ID, prefixed `plan_`.
    </ResponseField>

    <ResponseField name="account" type="object | null" required>
      Account that sells this variant; `null` for standalone invoice variants.

      <Accordion title="Properties" defaultOpen={true}>
        <ResponseField name="id" type="string" required>
          Account ID, prefixed `biz_`.
        </ResponseField>

        <ResponseField name="title" type="string" required>
          Account display name.
        </ResponseField>
      </Accordion>
    </ResponseField>

    <ResponseField name="adaptive_pricing_enabled" type="boolean" required>
      Whether adaptive pricing is enabled for this variant. Raw setting — does not
      check processor compatibility or feature flags.
    </ResponseField>

    <ResponseField name="attributes" type="object | null" required>
      Attribute values that distinguish this variant within its product, as a map of attribute name to value, e.g. `\{"color": "Blue", "size": "Large"}`. Names are snake\_case identifiers and come back in alphabetical order. Every attributed variant on a product carries the same attribute names and a distinct set of values; the product lists the full option set as `variant_attributes`. `null` when the variant has no attributes.
    </ResponseField>

    <ResponseField name="billing_period" type="number | null" required>
      Number of days between recurring charges, such as 30 for monthly or 365 for
      annual. `null` for one-time variants.
    </ResponseField>

    <ResponseField name="cancel_discount_intervals" type="number | null" required>
      Billing intervals the cancellation discount applies to (`0` forever, `1` first
      payment, or a month count). `null` when none is offered or the actor lacks the
      `plan:basic:read` scope.
    </ResponseField>

    <ResponseField name="cancel_discount_percentage" type="number | null" required>
      Cancellation discount as a whole-number percentage. `null` when none is
      offered or the actor lacks the `plan:basic:read` scope.
    </ResponseField>

    <ResponseField name="checkout_styling" type="object | null" required>
      Variant-level checkout styling (`background_color`, `button_color`,
      `font_family`, `border_style`); `null` inherits the account default.
    </ResponseField>

    <ResponseField name="collect_tax" type="boolean" required>
      Whether tax is collected on purchases of this variant, based on the account's
      tax configuration.
    </ResponseField>

    <ResponseField name="created_at" type="string" required>
      When the variant was created, as an ISO 8601 timestamp.
    </ResponseField>

    <ResponseField name="currency" type="string" required>
      Three-letter ISO currency code for this variant's prices.

      Available options: `usd`, `sgd`, `inr`, `aud`, `brl`, `cad`, `dkk`, `eur`, `nok`, `gbp`, `sek`, `chf`, `hkd`, `huf`, `jpy`, `mxn`, `myr`, `pln`, `czk`, `nzd`, `aed`, `eth`, `ape`, `cop`, `ron`, `thb`, `bgn`, `idr`, `dop`, `php`, `try`, `krw`, `twd`, `vnd`, `pkr`, `clp`, `uyu`, `ars`, `zar`, `dzd`, `tnd`, `mad`, `kes`, `kwd`, `jod`, `all`, `xcd`, `amd`, `bsd`, `bhd`, `bob`, `bam`, `khr`, `crc`, `xof`, `egp`, `etb`, `gmd`, `ghs`, `gtq`, `gyd`, `ils`, `jmd`, `mop`, `mga`, `mur`, `mdl`, `mnt`, `nad`, `ngn`, `mkd`, `omr`, `pyg`, `pen`, `qar`, `rwf`, `sar`, `rsd`, `lkr`, `tzs`, `ttd`, `uzs`, `rub`, `btc`, `cny`, `usdt`, `kzt`, `awg`, `whop_usd`, `xau`
    </ResponseField>

    <ResponseField name="custom_fields" type="object[]" required>
      Custom input fields collected on the checkout form.

      <Accordion title="Properties" defaultOpen={true}>
        <ResponseField name="id" type="string" required>
          Custom field ID, prefixed `field_`.
        </ResponseField>

        <ResponseField name="field_type" type="string" required>
          Custom field input type.

          Available options: `text`
        </ResponseField>

        <ResponseField name="name" type="string" required>
          Field label shown to customer at checkout.
        </ResponseField>

        <ResponseField name="order" type="number" required>
          Field position on checkout form.
        </ResponseField>

        <ResponseField name="placeholder" type="string | null" required>
          Placeholder text shown in the empty field. `null` if none is set.
        </ResponseField>

        <ResponseField name="required" type="boolean" required>
          Whether the customer must complete this field to check out.
        </ResponseField>
      </Accordion>
    </ResponseField>

    <ResponseField name="deletable" type="boolean | null" required>
      Whether the variant can be deleted (it has no memberships or waitlist
      entries). `null` unless the actor has the `plan:basic:read` scope on the
      variant's account.
    </ResponseField>

    <ResponseField name="description" type="string | null" required>
      Customer-visible variant description. Maximum 1000 characters. `null` if no
      description is set.
    </ResponseField>

    <ResponseField name="effective_payment_method_configuration" type="object | null" required>
      The configuration governing a checkout for this variant, resolved through every layer (the variant's own and the account's) — the shape a session's `payment_method_configuration` carries. Apply it over the payment method types catalogue for the offerable set. `null` means platform defaults; `payment_method_configuration` stays the variant's own editable override.

      <Accordion title="Properties" defaultOpen={true}>
        <ResponseField name="disabled" type="string[]" required>
          Payment methods this checkout withholds, even when the defaults would offer
          them.
        </ResponseField>

        <ResponseField name="enabled" type="string[]" required>
          Payment methods this checkout offers on top of whatever the defaults provide.
        </ResponseField>

        <ResponseField name="include_platform_defaults" type="boolean" required>
          Whether Whop's default set is the starting point. When `false`, only `enabled` is offered.
        </ResponseField>
      </Accordion>
    </ResponseField>

    <ResponseField name="expiration_days" type="number | null" required>
      Access duration in days for expiration-based variants, such as 365 for a
      one-year pass. `null` for variants without an expiration.
    </ResponseField>

    <ResponseField name="formatted_price" type="string" required>
      Human-readable price for display (currency + interval), e.g. "\$10 / month".
    </ResponseField>

    <ResponseField name="image" type="object | null" required>
      Pricing-tier image (`url`, `blurhash`) shown on the product page; `null` when
      no image is set.
    </ResponseField>

    <ResponseField name="initial_price" type="number" required>
      Initial purchase price in variant currency.
    </ResponseField>

    <ResponseField name="initial_price_due" type="object" required>
      Total charged at checkout for one unit, before promo codes and tax: `initial_price` plus the first `renewal_price` for recurring variants, or `initial_price` alone while a free trial applies. The trial does not apply when the viewing user has already used one for this variant.

      <Accordion title="Properties" defaultOpen={true}>
        <ResponseField name="amount" type="string" required>
          The amount in major units, as an exact decimal string — `"10.00"` is ten
          dollars. A string so no float rounds it in transit.
        </ResponseField>

        <ResponseField name="currency" type="string" required>
          Three-letter ISO 4217 currency code, lowercase.
        </ResponseField>

        <ResponseField name="decimals" type="integer" required>
          How many decimal places the amount CARRIES — the precision the charge itself
          runs at.
        </ResponseField>

        <ResponseField name="display_decimals" type="integer" required>
          How many decimal places to SHOW. Usually equal to `decimals`, and deliberately not always: COP is charged in centavos but written in whole pesos, so it is `2` and `0`. Format the number in your own locale using this.
        </ResponseField>
      </Accordion>
    </ResponseField>

    <ResponseField name="internal_notes" type="string | null" required>
      Private notes not shown to customers. `null` unless the actor has the
      `plan:basic:read` scope on the variant's account.
    </ResponseField>

    <ResponseField name="invoice" type="object | null" required>
      Invoice this variant was generated for; `null` unless created for an invoice.
    </ResponseField>

    <ResponseField name="member_count" type="number | null" required>
      Active memberships through this variant. `null` unless the actor has the
      `plan:basic:read` scope on the variant's account.
    </ResponseField>

    <ResponseField name="metadata" type="object | null" required>
      Custom key-value pairs stored on the variant. Included in webhook payloads for
      payment and membership events. Maximum 50 keys, 100 characters per key, 500
      characters per value. The reserved keys `custom_cta` and `custom_cta_url`,
      when set, override the product's checkout call to action for this variant.
    </ResponseField>

    <ResponseField name="offer_cancel_discount" type="boolean | null" required>
      Whether a cancellation discount is offered. `null` unless the actor has the
      `plan:basic:read` scope on the variant's account.
    </ResponseField>

    <ResponseField name="payment_method_configuration" type="object | null" required>
      Payment method configuration (`enabled`, `disabled`,
      `include_platform_defaults`); `null` when variant uses default settings.
    </ResponseField>

    <ResponseField name="plan_type" type="string" required>
      Billing model for this variant.

      Available options: `renewal`, `one_time`
    </ResponseField>

    <ResponseField name="product" type="object | null" required>
      Product this variant belongs to; `null` for standalone variants.
    </ResponseField>

    <ResponseField name="purchase_url" type="string" required>
      URL where customers can purchase this variant directly.
    </ResponseField>

    <ResponseField name="release_method" type="string" required>
      Sales method for this variant.

      Available options: `buy_now`, `waitlist`
    </ResponseField>

    <ResponseField name="renewal_price" type="number" required>
      Recurring price charged every billing period.
    </ResponseField>

    <ResponseField name="sku" type="string | null" required>
      Stock keeping unit, free text set by the seller (e.g. `TSHIRT-LARGE-BLUE`).
      Not enforced unique. `null` when unset.
    </ResponseField>

    <ResponseField name="split_pay_required_payments" type="number | null" required>
      Installment payments required before the subscription pauses. Must be greater
      than 1. `null` if split pay is not configured.
    </ResponseField>

    <ResponseField name="stock" type="number | null" required>
      Units available for purchase. `null` unless the actor has the
      `plan:basic:read` scope on the variant's account.
    </ResponseField>

    <ResponseField name="strike_through_initial_price" type="number | null" required>
      Original initial price shown with a strikethrough, in the variant's currency.
      `null` when no strikethrough is set.
    </ResponseField>

    <ResponseField name="strike_through_renewal_price" type="number | null" required>
      Original renewal price shown with a strikethrough, in the variant's currency.
      `null` when no strikethrough is set.
    </ResponseField>

    <ResponseField name="tax_type" type="string" required>
      How tax is handled for this variant, including whether tax is included in the price, added at checkout, or not configured.

      Available options: `inclusive`, `exclusive`, `unspecified`
    </ResponseField>

    <ResponseField name="three_ds_level" type="string | null" required>
      3D Secure behavior for supported on-session card payments. `mandate_challenge` requires a 3DS challenge before payment processing; `mandate_if_required` mandates a challenge only when the payment processor requires it; `frictionless_if_required` uses the regular frictionless 3DS flow. Payments of \$1,000 or more use `mandate_if_required` unless `mandate_challenge` is selected. Risk and authentication recovery requirements can override the preference. `null` inherits the account default.

      Available options: `mandate_challenge`, `mandate_if_required`, `frictionless_if_required`
    </ResponseField>

    <ResponseField name="title" type="string | null" required>
      Variant display name shown to customers. Maximum 30 characters. A variant
      created without one defaults to its attribute values joined with `/`. `null`
      if no title has been set.
    </ResponseField>

    <ResponseField name="trial_period_days" type="number | null" required>
      Free trial days before the first renewal charge. `null` if no trial is
      configured or the user has already used a trial for this variant.
    </ResponseField>

    <ResponseField name="unlimited_stock" type="boolean" required>
      Whether the variant has unlimited stock. When `true`, the `stock` field is
      ignored; waitlist variants always report `true`.
    </ResponseField>

    <ResponseField name="updated_at" type="string" required>
      When the variant was last updated, as an ISO 8601 timestamp.
    </ResponseField>

    <ResponseField name="visibility" type="string" required>
      Controls where this variant can be seen. When `hidden`, the variant is reachable only by its direct link.

      Available options: `visible`, `hidden`, `archived`, `quick_link`
    </ResponseField>
  </Column>

  <Column>
    <div className="api-resource-sticky-example">
      ```json Variant theme={null}
      {
      	"id": "plan_xxxxxxxxxxxxx",
      	"account": {
      		"id": "biz_xxxxxxxxxxxxxx",
      		"title": "Pickaxe"
      	},
      	"adaptive_pricing_enabled": true,
      	"attributes": {
      		"seats": "5",
      		"tier": "Pro"
      	},
      	"billing_period": 30,
      	"cancel_discount_intervals": null,
      	"cancel_discount_percentage": null,
      	"checkout_styling": null,
      	"collect_tax": true,
      	"created_at": "2023-12-01T05:00:00.401Z",
      	"currency": "usd",
      	"custom_fields": [
      		{
      			"id": "cusf_xxxxxxxxxxxx",
      			"field_type": "text",
      			"name": "Discord username",
      			"order": 0,
      			"placeholder": "e.g. pickaxe_user",
      			"required": true
      		}
      	],
      	"deletable": false,
      	"description": "Monthly access to Pickaxe Analytics.",
      	"expiration_days": null,
      	"formatted_price": "$29.00 / month",
      	"image": null,
      	"initial_price": 29,
      	"internal_notes": "Standard monthly variant",
      	"invoice": null,
      	"member_count": 42,
      	"metadata": {
      		"external_plan_id": "monthly",
      		"custom_cta": "subscribe",
      		"custom_cta_url": "https://example.com/wash-club"
      	},
      	"offer_cancel_discount": false,
      	"payment_method_configuration": {
      		"enabled": ["card"],
      		"disabled": [],
      		"include_platform_defaults": true
      	},
      	"effective_payment_method_configuration": {
      		"enabled": ["card"],
      		"disabled": [],
      		"include_platform_defaults": true
      	},
      	"plan_type": "renewal",
      	"product": {
      		"id": "prod_xxxxxxxxxxxxx",
      		"title": "Pickaxe Analytics"
      	},
      	"purchase_url": "https://whop.com/pickaxe-analytics/checkout/plan_xxxxxxxxxxxxx",
      	"release_method": "buy_now",
      	"renewal_price": 29,
      	"initial_price_due": {
      		"currency": "usd",
      		"amount": "29.00",
      		"decimals": 2,
      		"display_decimals": 2
      	},
      	"sku": "PICKAXE-PRO-5-MONTHLY",
      	"split_pay_required_payments": null,
      	"stock": null,
      	"strike_through_initial_price": null,
      	"strike_through_renewal_price": null,
      	"tax_type": "exclusive",
      	"three_ds_level": "frictionless_if_required",
      	"title": "Pro / 5 seats",
      	"trial_period_days": 7,
      	"unlimited_stock": true,
      	"updated_at": "2023-12-01T05:00:00.401Z",
      	"visibility": "visible"
      }
      ```
    </div>
  </Column>
</Columns>
