import React from "react";
import CodeSnippet from "$app/components/ui/CodeSnippet";
import { ApiEndpoint } from "../ApiEndpoint";
import { ApiParameter, ApiParameters } from "../ApiParameters";
import { ApiResponseFields, renderFields } from "../ApiResponseFields";
import { CATEGORY_FIELDS, PRODUCT_FIELDS, PRODUCT_LIST_FIELDS } from "../responseFieldDefinitions";
const ProductResponseFields = () => (
{renderFields([
{ name: "success", type: "boolean", description: "Whether the request succeeded" },
{ name: "products", type: "array", description: "Array of product objects", children: PRODUCT_LIST_FIELDS },
{
name: "next_page_key",
type: "string",
description: "Opaque cursor to pass as page_key to fetch the next page",
condition: "present when more results follow",
},
{
name: "next_page_url",
type: "string",
description: "Path-relative URL (with query string) for the next page of results",
condition: "present when more results follow",
},
])}
);
const SingleProductResponseFields = () => (
{renderFields([
{ name: "success", type: "boolean", description: "Whether the request succeeded" },
{ name: "product", type: "object", description: "The product object", children: PRODUCT_FIELDS },
])}
);
const CreateProductResponseFields = () => (
{renderFields([
{ name: "success", type: "boolean", description: "Whether the request succeeded" },
{ name: "product", type: "object", description: "The product object", children: PRODUCT_FIELDS },
{
name: "warning",
type: "string",
description:
"Explains why the product was saved as a draft instead of being published (for example, an unconfirmed email address; publish it later with POST /v2/products/:id/enable), or that a post-publish step failed. Check the product's published field for its actual state.",
condition: "present when publishing did not fully succeed",
},
])}
);
const CategoriesResponseFields = () => (
{renderFields([
{ name: "success", type: "boolean", description: "Whether the request succeeded" },
{ name: "categories", type: "array", description: "Flat list of product categories", children: CATEGORY_FIELDS },
])}
);
const UpdateProductResponseFields = () => (
{renderFields([
{ name: "success", type: "boolean", description: "Whether the request succeeded" },
{ name: "product", type: "object", description: "The product object", children: PRODUCT_FIELDS },
{
name: "warning",
type: "string",
description:
"Warning about offer codes that became invalid for the product, or custom HTML that has no buy element.",
condition: "present when there is an advisory warning after the update",
},
{
name: "file_id_mappings",
type: "object",
description:
"Map of client file ids (files[][id] or files[][external_id]) to the canonical ProductFile id. Use this after attaching a newly uploaded file so a follow-up variant or rich_content write can reference it without GET-diffing the file list.",
condition: "present when at least one files entry remapped a client id",
},
])}
);
const CustomHtmlDocumentation = () => (
Custom HTML landing pages
A product can have one custom HTML landing page, stored in its custom_html field. While it's set and
the product is published, buyers see it instead of the default product page. Authenticate with a Bearer token that
has the edit_products scope.
GET /v2/products/:id returns the custom_html field.
PUT /v2/products/:id sets it; send null or an empty string to clear it.
POST /v2/products/:id/preview_custom_html returns the sanitized HTML and a sanitization report
without saving — use it to iterate before you publish.
Both PUT and preview return a sanitization_report listing what was stripped.
Both PUT and preview return a top-level warning if the custom HTML has no{" "}
data-gumroad-action="buy" element or gumroad:checkout postMessage.
A successful PUT also returns previous_custom_html (the prior value, for one-step
rollback) and the live landing_url.
Links to your own Gumroad pages (your storefront, your other products) navigate the visitor's tab as ordinary
links — no target="_blank" needed. Links to other sites still open in a new tab.
Only the latest version is stored — there's no history, so keep your source under version control.
The HTML is capped at 500,000 characters.
Rate limits per token: 30 PUTs/min, 60 previews/min.
Your HTML is sanitized — disallowed tags and attributes are stripped — then served inside a sandboxed iframe (
sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
).
It can:
Run inline JavaScript for animations, scroll effects, sticky headers, and modals.
Load scripts from the Tailwind, jsDelivr, and unpkg CDNs.
Load fonts from Google Fonts and Bunny Fonts.
Load images and media from Gumroad only — e.g. your product's covers and thumbnail.
Submit forms in-page with JavaScript.
Open links in a new tab or window.
It can't:
Read your Gumroad cookies or session — it runs on an opaque origin.
Touch the parent page.
Download files. The sandbox omits allow-downloads, so a link to a PDF, zip, or any other file the
browser would download is cancelled — silently, with no error anywhere, so the link just looks dead.{" "}
target="_blank" does not work around it. Deliver the file as product content and link to
the product instead.
Make fetch, XHR, or WebSocket requests (connect-src 'none').
Load images or media from any non-Gumroad host.
Submit forms to external URLs — off-site action attributes are stripped.
Every external load is restricted to Gumroad's CDN (images and media) or the named font and script CDNs above, so
the page has no arbitrary-host network channel — it can't beacon data off to a server you control.
Live values and buy buttons
Mark elements with data attributes that Gumroad fills in server-side so the page always shows current values and a
working checkout button:
data-gumroad-field="name|price|description|rating|review-count" — the element's contents are
replaced with the product's current value (HTML-escaped). price is the amount a first-time buyer
pays: your default offer code is applied, and a membership is quoted at its default recurrence — matching the
native product page this HTML replaces. rating and review-count leave the element's
existing contents alone when reviews are hidden on the product or it has none yet, so write your own fallback
text inside them.
data-gumroad-action="buy" — wires the element up to launch the Gumroad checkout. Works on any tag (
<a>, <button>, <div>).
For products with selection state, set the choice directly on the buy element. Invalid values silently fall back
to the product defaults — they won't break the page.
data-gumroad-option="<variant name>" — products with variants/versions/tiers.
data-gumroad-quantity="<integer>" — products with quantity enabled.
data-gumroad-price="<decimal>" — pay-what-you-want products; major units (e.g.{" "}
"9.99").