---
title: "Create Product"
method: POST
path: "/products"
tags: ["Products"]
---

# Create Product

`POST /products`

Creates a new product for an account.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `account_id` string — The unique identifier of the account to create this product for.
  - `collect_shipping_address` boolean, nullable — Whether to collect a shipping address at checkout.
  - `custom_cta` 'get_access' | 'join' | 'order_now' | 'shop_now' | 'call_now' | 'donate_now' | 'contact_us' | 'sign_up' | 'subscribe' | 'purchase' | 'get_offer' | 'apply_now' | 'complete_order' | 'null', nullable — The call-to-action button label.
  - `custom_cta_url` string, nullable — A URL the call-to-action button links to.
  - `custom_statement_descriptor` string, nullable — Custom bank statement descriptor. Must start with WHOP*.
  - `description` string, nullable — A written description displayed on the product page. Maximum 1,500 characters.
  - `gallery_images` object[], nullable — Images or videos displayed in the product gallery, in display order. Replaces the existing gallery. Send an empty array to clear it; omit or pass null to leave it unchanged. A banner image does not populate the gallery.
    - `direct_upload_id` string — The signed ID of a completed direct upload, as an alternative to id.
    - `id` string — The tag of an already-uploaded attachment.
  - `global_affiliate_percentage` number, nullable — The commission rate affiliates earn.
  - `global_affiliate_status` 'enabled' | 'disabled' — The enrollment status in the global affiliate program.
  - `headline` string, nullable — A short marketing headline for the product page. Maximum 80 characters.
  - `labels` string[], nullable — Labels used to group products into collections. Stored lowercased and de-duplicated. Maximum 20 labels, 50 characters each.
  - `member_affiliate_percentage` number, nullable — The commission rate members earn.
  - `member_affiliate_status` 'enabled' | 'disabled' — The enrollment status in the member affiliate program.
  - `metadata` object, nullable — Custom key-value pairs to store on the product.
  - `product_tax_code_id` string, nullable — The unique identifier of the tax classification code. See the available [product categories](https://docs.numeral.com/essentials/product-categories).
  - `redirect_purchase_url` string, nullable — A URL to redirect the customer to after purchase.
  - `route` string, nullable — The URL slug for the product's public link.
  - `send_welcome_message` boolean, nullable — Whether to send an automated welcome message via support chat when a user joins this product. Defaults to true.
  - `title` string, required — The display name of the product. Maximum 80 characters.
  - `visibility` string — Whether the product is visible to customers.

## Response `200`

product created

- Product
  - `account` object, nullable, required — Account that sells this product.
  - `average_review_rating` number, required — Average star rating across published reviews for this product, from `1.0` to `5.0`. Returns `0.0` when no published-review rating is available.
  - `created_at` string, required — When the product was created, as an ISO 8601 timestamp.
  - `custom_cta` 'get_access' | 'join' | 'order_now' | 'shop_now' | 'call_now' | 'donate_now' | 'contact_us' | 'sign_up' | 'subscribe' | 'purchase' | 'get_offer' | 'apply_now' | 'complete_order' | 'null', nullable, required — Call-to-action button label shown on the product purchase page.
  - `custom_cta_url` string, nullable, required — URL the call-to-action button links to instead of checkout.
  - `custom_statement_descriptor` string, nullable, required — Custom text label on customer's bank statement.
  - `default_plan` ProductPublicPlan, required
    - `billing_period` number, nullable, required — Number of days between recurring charges, such as 30 for monthly or 365 for annual. `null` for one-time plans.
    - `expiration_days` number, nullable, required — Access duration in days for expiration-based plans. `null` for plans without an expiration.
    - `id` string, required — Plan ID, prefixed `plan_`.
    - `initial_price` Money, required
      - `amount` 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.
      - `currency` string, required — Three-letter ISO 4217 currency code, lowercase.
      - `decimals` integer, required — How many decimal places the amount CARRIES — the precision the charge itself runs at.
      - `display_decimals` 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.
    - `plan_type` 'renewal' | 'one_time', required — Billing model for this plan: `one_time` or `renewal`.
    - `renewal_price` Money, required
      - `amount` 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.
      - `currency` string, required — Three-letter ISO 4217 currency code, lowercase.
      - `decimals` integer, required — How many decimal places the amount CARRIES — the precision the charge itself runs at.
      - `display_decimals` 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.
    - `title` string, nullable, required — Plan display name shown to customers. `null` if no title has been set.
    - `unlimited_stock` boolean, required — Whether the plan has unlimited stock.
    - `visibility` 'visible' | 'hidden' | 'archived' | 'quick_link', required — Where this plan can be seen. `visible` plans appear on the product page.
  - `description` string, nullable, required — Written description displayed on the product page. `null` if none is set.
  - `external_identifier` string, nullable, required — External identifier stored on the product for your own reference.
  - `gallery_images` ProductGalleryImage[], required
    - `content_type` string, nullable, required — Uploaded file MIME type, such as image/jpeg.
    - `id` string, required — Gallery image ID.
    - `url` string, nullable, required — Pre-optimized URL for rendering this image on the client.
  - `global_affiliate_percentage` number, nullable, required — Commission rate affiliates earn through the global affiliate program.
  - `global_affiliate_status` 'enabled' | 'disabled' | 'null', nullable, required — Enrollment status in the global affiliate program.
  - `headline` string, nullable, required — Short marketing headline displayed on product page.
  - `id` string, required — Product ID, prefixed `prod_`.
  - `labels` string[], required
  - `marketplace_status` 'not_available' | 'pending_review' | 'live_marketplace', required — Listing state on the whop.com marketplace. `pending_review` means submitted and awaiting review; `live_marketplace` means approved and discoverable.
  - `member_affiliate_percentage` number, nullable, required — Commission rate members earn through the member affiliate program.
  - `member_affiliate_status` 'enabled' | 'disabled' | 'null', nullable, required — Enrollment status in the member affiliate program.
  - `member_count` number, required — Active memberships for this product; 0 if public member counts are disabled.
  - `metadata` object, nullable, required — Custom key-value pairs stored on the product.
  - `owner_user` object, nullable, required — User who owns the account selling this product.
  - `product_tax_code` object, nullable, required — Tax classification code for this product, or `null` if no tax code is set.
  - `published_reviews_count` number, required — Published customer reviews for this product.
  - `route` string, required — URL slug for the product's public link.
  - `title` string, required — Product display name shown to customers.
  - `updated_at` string, required — When the product was last updated, as an ISO 8601 timestamp.
  - `variant_attributes` object, nullable, required — The option set the product's variants span, as a map of attribute name to the values in use, e.g. `{"color": ["Blue", "Red"], "size": ["S", "M", "L"]}`. Derived from the visible, non-invoice plans that carry `attributes`: keys alphabetical, values in the order the plans were created. Read-only. `null` when the product has no variants.
  - `variants` PlanListItem[], nullable, required
    - `account` AccountSummary, required
      - `id` string, required — Account ID, prefixed `biz_`.
      - `title` string, required — Account display name.
    - `adaptive_pricing_enabled` boolean, required — Whether adaptive pricing is enabled for this plan. Raw setting — does not check processor compatibility or feature flags.
    - `attributes` object, nullable, required — Attribute values that make this plan one variant of 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 variant plan 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` for a plan that is not a variant.
    - `billing_period` number, nullable, required — Number of days between recurring charges, such as 30 for monthly or 365 for annual. `null` for one-time plans.
    - `cancel_discount_intervals` number, nullable, 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.
    - `cancel_discount_percentage` number, nullable, required — Cancellation discount as a whole-number percentage. `null` when none is offered or the actor lacks the `plan:basic:read` scope.
    - `checkout_styling` object, nullable, required — Plan-level checkout styling (`background_color`, `button_color`, `font_family`, `border_style`); `null` inherits the account default.
    - `created_at` string, required — When the plan was created, as an ISO 8601 timestamp.
    - `currency` string, required — Three-letter ISO currency code for this plan's prices.
    - `custom_fields` PlanCustomField[], required
      - `field_type` 'text', required — Custom field input type.
      - `id` string, required — Custom field ID, prefixed `field_`.
      - `name` string, required — Field label shown to customer at checkout.
      - `order` number, required — Field position on checkout form.
      - `placeholder` string, nullable, required — Placeholder text shown in the empty field. `null` if none is set.
      - `required` boolean, required — Whether the customer must complete this field to check out.
    - `description` string, nullable, required — Customer-visible plan description. Maximum 1000 characters. `null` if no description is set.
    - `expiration_days` number, nullable, required — Access duration in days for expiration-based plans, such as 365 for a one-year pass. `null` for plans without an expiration.
    - `formatted_price` string, required — Human-readable price for display (currency + interval), e.g. "$10 / month".
    - `id` string, required — Plan ID, prefixed `plan_`.
    - `image` object, nullable, required — Pricing-tier image (`url`, `blurhash`) shown on the product page; `null` when no image is set.
    - `initial_price` number, required — Initial purchase price in plan currency.
    - `initial_price_due` Money, required
      - `amount` 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.
      - `currency` string, required — Three-letter ISO 4217 currency code, lowercase.
      - `decimals` integer, required — How many decimal places the amount CARRIES — the precision the charge itself runs at.
      - `display_decimals` 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.
    - `internal_notes` string, nullable, required — Private notes not shown to customers. `null` unless the actor has the `plan:basic:read` scope on the plan's account.
    - `invoice` object, nullable, required — Invoice this plan was generated for; `null` unless created for an invoice.
    - `member_count` number, nullable, required — Active memberships through this plan. `null` unless the actor has the `plan:basic:read` scope on the plan's account.
    - `metadata` object, nullable, required — Custom key-value pairs stored on the plan. 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 plan.
    - `offer_cancel_discount` boolean, nullable, required — Whether a cancellation discount is offered. `null` unless the actor has the `plan:basic:read` scope on the plan's account.
    - `payment_method_configuration` object, nullable, required — Payment method configuration (`enabled`, `disabled`, `include_platform_defaults`); `null` when plan uses default settings.
    - `plan_type` 'renewal' | 'one_time', required — Billing model for this plan.
    - `product` object, nullable, required — Product this plan belongs to; `null` for standalone plans.
    - `purchase_url` string, required — URL where customers can purchase this plan directly.
    - `release_method` 'buy_now' | 'waitlist', required — Sales method for this plan.
    - `renewal_price` number, required — Recurring price charged every billing period.
    - `sku` string, nullable, required — Stock keeping unit, free text set by the seller (e.g. `TSHIRT-LARGE-BLUE`). Not enforced unique. `null` when unset.
    - `split_pay_required_payments` number, nullable, required — Installment payments required before the subscription pauses. Must be greater than 1. `null` if split pay is not configured.
    - `stock` number, nullable, required — Units available for purchase. `null` unless the actor has the `plan:basic:read` scope on the plan's account.
    - `strike_through_initial_price` number, nullable, required — Original initial price shown with a strikethrough, in the plan's currency. `null` when no strikethrough is set.
    - `strike_through_renewal_price` number, nullable, required — Original renewal price shown with a strikethrough, in the plan's currency. `null` when no strikethrough is set.
    - `three_ds_level` 'mandate_challenge' | 'mandate_if_required' | 'frictionless_if_required' | 'null', nullable, 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.
    - `title` string, nullable, required — Plan 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.
    - `trial_period_days` number, nullable, 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 plan.
    - `unlimited_stock` boolean, required — Whether the plan has unlimited stock. When `true`, the `stock` field is ignored; waitlist plans always report `true`.
    - `updated_at` string, required — When the plan was last updated, as an ISO 8601 timestamp.
    - `visibility` 'visible' | 'hidden' | 'archived' | 'quick_link', required — Controls where this plan can be seen. When `hidden`, the plan is reachable only by its direct link.
  - `verified` boolean, required — Whether the product has been verified by Whop.
  - `visibility` string, nullable, required — Whether the product is publicly visible, hidden, or archived.

## Other responses

- `401` — Unauthorized
- `409` — Conflict

## Changes

> 72 revisions in range; 1 not diffed.

- **2026-09-24** `7fcefabddb0a` — 3 warning, 2 info
  - the `description` request property's maxLength was set to `1500`
  - the `headline` request property's maxLength was set to `80`
  - the `title` request property's maxLength was set to `80`
  - added the required property `variant_attributes` to the response with the `200` status
  - …1 more
- **2026-09-12** `1660b8d62204` — 1 info
  - added the new optional request property `gallery_images`
- **2026-09-10** `2a25564a92d5` — 1 info
  - added the required property `average_review_rating` to the response with the `200` status
- **2026-08-24** `e91d507bda45` — 1 info
  - added the required property `default_plan` to the response with the `200` status
- **2026-08-19** `23dfbe1a0d1f` — 2 info
  - added the new optional request property `labels`
  - added the required property `labels` to the response with the `200` status

[Full history](https://skmtc.dev/whop/apis/whop-api/changes/products/post.md)

---

[API](https://skmtc.dev/whop/apis/whop-api.md) · [All operations](https://skmtc.dev/whop/apis/whop-api/llms.txt) · [OpenAPI document](https://skmtc.dev/whop/apis/whop-api/revisions/86b97292852d?raw)
