gumroad — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited gumroad (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use gumroad (Gumroad CLI) to query and manage Gumroad data.
Always follow these rules:
--no-input to prevent interactive prompts from blocking.--json for programmatic access.--json --jq <expr> together to extract exactly what you need.files abort, files complete replay, product updates that remove files, or products content set when omitted page IDs will be deleted), add --yes to skip confirmation.--quiet to suppress spinners and status messages.--dry-run to preview mutating requests without executing them.--page-delay 200ms with --all to avoid rate limits on large datasets.--price 10.00 for $10), not cents. The CLI converts internally. Use --currency eur to change currency.--draft to save an unpublished draft instead, then gumroad products publish <id> to make it live; gumroad products unpublish <id> takes a published product down. If the account cannot publish yet (unconfirmed email, no payout method), create still succeeds but saves a draft and prints the reason.gumroad products page preview <id> ./landing.html to run the backend sanitizer without writing, gumroad products page publish <id> ./landing.html to store the page, gumroad products page clear <id> --yes to remove it, and gumroad products page url <id> to print the live URL. --dry-run only previews the CLI request body; it does not call the backend sanitizer. Inspect .sanitization_report in preview and publish JSON output for server-side changes.gumroad user page preview ./landing.html, gumroad user page publish ./landing.html (read from stdin with -), gumroad user page clear --yes, and gumroad user page url (prints the public profile URL and its /landing/embed URL). A profile has no buy button, so omit data-gumroad-action="buy" and the checkout data attributes; link to products instead.<username>.gumroad.com/<slug>) use gumroad pages list, gumroad pages create --title <title> [--slug <slug>] [path] (with an HTML path or - the page is created as custom HTML; without one it starts empty for the in-app editor), gumroad pages pull <slug> to download a page's existing custom HTML so pull → edit → push is a real round trip (writes <slug>.html; -o <path> to choose, -o - for stdout; refuses to overwrite without --force; errors with a hint when the page has no custom HTML), gumroad pages scaffold <slug> to generate starter HTML for a rich-text page or a default profile from a static snapshot of its current render (same flags as pull; pushing the scaffold converts the page to custom HTML and replaces the dynamic storefront/editor experience), gumroad pages push <slug> ./page.html to replace a page with custom HTML, gumroad pages preview ./page.html to run the backend sanitizer without publishing, and gumroad pages seo <slug> [--title <title>] [--description <text>] to show or set the search title and description that search engines and shared links show (an empty value goes back to the default; an omitted flag leaves that field alone; seo profile sets the home page's fields through PUT /user). The loop for going custom: pull (or scaffold when there's no custom HTML yet) → edit → preview → push. --json/--jq on pull/scaffold still write the file and additionally print the raw API response; combining them with -o - is rejected since both would own stdout. The special slug profile targets the profile landing page (pull profile downloads your published custom HTML, scaffold profile snapshots the default storefront render when none is published; push uses the same endpoints as user page publish). Custom HTML pages require the seller's custom_html_pages feature to be enabled; without it writes fail with an access message. Writes to slugged pages (pages create/pages push <slug>) also require the token to carry the edit_profile scope — tokens minted before the CLI requested that scope get a 403 telling them to re-run gumroad auth login.gumroad products content list <id> --json --no-input to inspect page IDs, gumroad products content get <id> --json --no-input to dump the shared rich_content page array, and gumroad products content set <id> content.json --dry-run --json --no-input to preview a whole-document replacement. Without an explicit path, whole-document set reads ./content.json; set --page reads ./page.json. Use --page <page_id> with get/set to edit one matching page object; set --page still sends a merged whole-document PUT. For a multi-page course, products content upload <id> course.json takes a manifest of {pages:[{id,title,files:[path|{path,name,description}]}]} and writes the product once per page instead of once per file, recording finished uploads and pages in a resume file (--state) so a rerun after an interruption never uploads or places anything twice. For per-variant content, pass both --variant <variant_id> and --category <cat_id>. Whole-document set deletes existing pages omitted from the JSON.data-gumroad-field="name", data-gumroad-field="price", data-gumroad-field="description", and data-gumroad-action="buy". To preselect checkout state, add data-gumroad-option="<variant name>", data-gumroad-quantity="<integer>", data-gumroad-price="<decimal>", or data-gumroad-recurrence="monthly|quarterly|biannually|yearly|every_two_years". Production validates these values and falls back to product defaults when invalid. Prefer anchors for buy CTAs so production can add a checkout href; non-anchor buy elements also post to checkout.<iframe> and says why in .sanitization_report. A video file hosted by Gumroad still plays in a <video> tag. To show a video from another site, link to it with target="_blank" so it opens in a new tab. To show a YouTube thumbnail in that link, download https://i.ytimg.com/vi/<video-id>/hqdefault.jpg, upload it with gumroad media upload, and use the returned .media.url in an <img> inside the link.gumroad emails send-preview <id> --json --no-input and inspect .preview_url before gumroad emails send <id> --yes --json --no-input. Creating with --send publishes and blasts immediately, so use --dry-run first and require explicit human approval. To schedule a send instead of blasting immediately, create the draft then gumroad emails schedule <id> --at "<RFC3339>" --json --no-input; gumroad emails unschedule <id> returns a scheduled email to draft.--dry-run before each write.gumroad auth status --json --no-input first. Agents can start seller auth with gumroad auth login --no-input and hand the printed approval URL to a human, or use an existing seller token via GUMROAD_ACCESS_TOKEN or gumroad auth login --with-token.--non-interactive and set GUMROAD_ADMIN_TOKEN; interactive shells can store an admin token with gumroad auth login --web.gumroad mcp to serve the public CLI commands as MCP tools over stdio.gumroad auth login, or pass GUMROAD_ACCESS_TOKEN in the MCP server environment. The server starts without a token, but calls return a login hint until credentials are available.Add this to your client's MCP configuration (use the absolute path to gumroad if it is not on the client's PATH):
{"mcpServers":{"gumroad":{"command":"gumroad","args":["mcp"]}}}Tools follow CLI leaf command paths with underscores, including hyphens converted to underscores: products_list, products_view, offer_codes_list, sales_refund. Input keys are the original long flag names; repeatable flags take arrays of strings, and positional arguments go in args (for example, {"args":["<product-id>"]}). Defaults stay with the CLI, and its validators report missing arguments or flags. Runnable groups such as gumroad user are exposed too (user reads the account); use user or products_list as a connection check.
Every call uses a fresh command with --json --no-input --quiet and automatically passes --yes where available, except marketing commands: those require the caller to supply yes: true after showing the exact text, account and link and obtaining seller confirmation. Mutations run immediately, without interactive confirmation; require approval in the MCP client before sending a mutating call. Use dry-run: true when supported to preview requests. Auth, admin, completion, skill, help, MCP itself, and hidden/deprecated commands are excluded. The server uses only the seller token, never the admin token.
Only connect trusted clients: tools have the same local file access as the CLI, including uploads and downloads. File paths are on the machine running the server. Stdin-based content input is unavailable; provide file paths or explicit flags instead. HTTP transport is not supported. Annotations are conservative: list/view/get/preview/download-style commands and non-page pull commands are marked read-only; pages_pull carries destructiveHint because it writes a local HTML file. Every other tool (including licenses_verify, which increments uses unless no-increment is true, pages_push and emails_send) also carries destructiveHint so clients ask before running it. They describe the command category, not a security boundary (downloads still write local files).
Most responses are wrapped in {"success": true, ...} with resource-specific keys:
user → .user, user update → .useruser page preview → .custom_html, .sanitization_reportuser page publish / user page clear → .custom_html, .previous_custom_html, .profile_url, .sanitization_reportuser page url → .profile_url, .has_landing_pagepages list → .pages[] (.slug, .title, .content, .custom_html, .url); pages create / pages push <slug> → .page; pages pull <slug> / pages scaffold <slug> → .page + .rendered_html (pull writes the page's .page.custom_html; scaffold writes .rendered_html, the static render snapshot); pages pull profile / pages scaffold profile → .custom_html, .rendered_html, .has_landing_page, .profile_url; pages push profile → profile shape (.custom_html, .previous_custom_html, .profile_url, .sanitization_report); pages preview → .custom_html, .sanitization_report; pages seo <slug> → .page (.seo_title, .seo_description, null when the default is used); pages seo profile → .user (.seo_title, .seo_description)refund-policy view/set → .refund_policyproducts list → .products[]products view → .productproducts content get → rich content page array directly, or one page object with --pageproducts content list → rich content page summary array directlyproducts content set → mutation envelope with .resultproducts bundle list / products bundle set → .product.bundle_products[] (product_id, variant_id, quantity, position)sales list → .sales[]sales buyers → .buyers[] (email, name, purchase_count, last_purchase_date, utm_source, utm_medium, utm_campaign, utm_term, utm_content)sales view → .sale (includes .currency, the ISO code the sale is priced in — the same currency a refund amount is read in)sales export → .status, .recipient_emailsales summary → .gross_cents, .net_cents, .breakdown[]marketing recommend → .channels[] (live entries include .handle, .action.id, .action.idempotency_key, .action.confirmation_token, .action.post_text, .action.link_url); marketing approve/schedule/cancel/status → .marketing_action, .handle; schedule also includes .intent_url, .connect_path. Inspect .marketing_action.status and .error_code: HTTP success is not proof of posting.emails list → .emails[], emails view/create/send/schedule/unschedule → .email, emails send-preview → .preview_url, emails delete → .messageworkflows list → .workflows[], workflows view → .workflow, workflows add-email/update-email → .emailpayouts list → .payouts[], payouts view/upcoming → .payoutsubscribers list → .subscribers[], subscribers view → .subscriberlicenses verify → .purchaseoffer-codes list → .offer_codes[]upsells list → .upsells[], upsells view/create/update → .upsell, upsells delete → .messagevariant-categories list → .variant_categories[]variants list → .variants[]files upload / files complete → .file_urlbuilds list → .builds[], builds deploy → mutation envelope with .result.buildmedia upload → .media (.id, .name, .url, .file_size), media list → .media[], media delete → .messageproducts create → .product (.product.published reports whether it went live; a blocked publish adds a top-level .warning); with media flags also .media[]products update with media flags → .product plus .media[]products covers add --image → .result.covers[], .result.main_cover_id, plus .result.media[]products covers add --url → .result.covers[], .result.main_cover_idproducts thumbnail set --image → .result.thumbnail, plus .result.media[]products thumbnail set --url → .result.thumbnailproducts page preview → .custom_html, .sanitization_reportproducts page publish / products page clear → .product.custom_html, .product.landing_url, .previous_custom_html, .sanitization_reportproducts update --custom-html → .product.custom_html, .product.landing_url, .previous_custom_html, .sanitization_reportproducts create/update --refund-period/--refund-fine-print and products view → .product.refund_policy (.refund_period is inherit or none/7/14/30/183, plus .title, .fine_print, .inherited)products write verb is flat: create, update, unpublish, and delete return top-level fields, but covers add, thumbnail set, and content set still wrap their payload in the {success, …, result} envelope — read those under .resultwebhooks list → .resource_subscriptions[]admin users info → .user (includes .user.stripe Stripe Connect state — connected, and when connected stripe_connect_account_id, stripe_dashboard_url, and a verification block of flags/counts or an error subfield when the live Stripe lookup failed — and .user.admin_links impersonate/user/purchases/stripe-dashboard URLs)admin users social-connections → .social_connections[] (stored verification, currently_linked, timestamps, nullable audience counts, and shared_identity_user_count) and optional .latest_shadow_evaluation (null or missing without a supplied snapshot; otherwise mode: historical_shadow, evaluated_on, recorded_at, stored score, unpaid_balance_cents, would_have_released, hold_source, signals). Historical shadow evidence is not current eligibility or payout authorization.admin users affiliates → .affiliates[]admin users comments list → .comments[]admin users comments add → .commentadmin users compliance → .compliance_info, .info_requests[]admin users credits add → .user_id, .credit.id, .credit.amount_cents, .credit.reason, .credit.crediting_user_id, .credit.created_atadmin users credits list → .credits[], .pagination.nextadmin users radar → .radar_stats, .recent_efws[]admin users purchases → .purchases[]admin users related → .related_users[], .truncated, .per_signal_limitadmin users mark-compliant, admin users suspend, admin users suspend-for-tos-violation → .status, .message, .user_idadmin products flag-for-tos-violation → .status, .message, .user_id, .product_idadmin products hold-sales, admin products release-sales-hold → .status (held, already_held, released, already_released), .sales_hold (id, active, unpublished_listing, held_at, held_by_id, released_at, released_by_id), .product (id, alive, published, draft, purchase_disabled_at, deleted_at, banned_at, unpublished_by_admin, active_sales_hold_id)admin announcements create, update, view, approve, send, cancel → .campaign (key, state, audience, content_digest); view adds .deliveries[] (state, reason, count)admin announcements preview → .campaign, .subscribed_count, .email (from, reply_to, subject, html_body)admin announcements list → .campaigns[]admin payouts list → .recent_payouts[], .pagination.next. Each payout carries stripe_transfer_id (a po_… payout or py_… destination payment, or null), bank_account (null for PayPal and debit-card payouts; otherwise bank_number routing/BIC, account_holder_full_name, account_type, currency), and trace_id (currently always null).admin payouts scheduled create → .message, .user_id, .scheduled_payoutadmin users refund-balance → .status, .message, .user_id, .count, .total_amount_cents, .currencyadmin users refund-all-for-fraud → .success, .user_id, .status (queued), .message, .purchases_to_refund, .block_buyersadmin purchases view → .purchaseadmin purchases refund-taxes → .message, .purchaseadmin purchases search → .purchases[], .has_more, .limitadmin purchases lookup → .purchases[]admin products list → .products[], admin products view → .productadmin stripe court-order-notice --dry-run → .status (dry_run), .already_sent, .delivery_status, .accepted_at, .comment_id (null unless the notice was already recorded), .user (user_id, email, name), .email (from, reply_to[], to[], subject, html_body)admin stripe court-order-notice → .status (queued or already_sent), .already_sent, .delivery_status (pending or accepted), .accepted_at (null until accepted), .comment_id, .user (user_id, email, name), .comment.content, .comment.created_at. Only already_sent means the mail service accepted the email; queued stays pending until a later call with the same key answers already_sent.Admin pagination models differ by command:
admin users affiliates, admin users comments list, admin users credits list, admin users radar, admin users purchases, and admin purchases lookup return .pagination.next as a cursor string. Pass it back with --cursor.admin products list returns .pagination.next as an integer page number. Pass it back with --page; use --per-page for page size.admin users related returns at most 50 related users per signal. Always inspect .truncated; when any signal is true, the result hit the cap and there is no cursor/page to fetch the rest.admin purchases search returns .has_more when the server capped results. --limit is server-capped at 25 and there is no continuation token.When creating or updating many products:
--dry-run --json to preview generated requests, and ask the user to confirm before mutating more than 5 products.gumroad products covers add <id> --image ./cover.jpg.Requires the seller's auto_marketing flag and an edit_emails token (or the API's legacy account scope). A flag-off or foreign resource returns 404. The commands reuse the same action and tagged link as the product editor's Share tab; no local marketing state is created.
gumroad marketing recommend <product-id-or-permalink> --json --no-input
gumroad marketing status <action-id> --json --no-input
# Show exact post_text, handle and link_url to the seller; obtain confirmation before --yes.
gumroad marketing approve <action-id> --yes --confirmation-token <reviewed-token> --json --no-input
# schedule means execute NOW in this version, not at a future date. Approve first.
gumroad marketing schedule <action-id> --yes --confirmation-token <reviewed-token> --json --no-input
gumroad marketing cancel <action-id> --yes --json --no-inputEach mutation fetches and prints the exact action preview to stderr before confirmation; quoted text escapes control characters. --dry-run still performs that GET but sends no POST. The CLI returns the server's opaque idempotency and confirmation keys unchanged. Copy or account changes during confirmation are refused; review again rather than silently retrying altered content. Repeating approve/schedule resolves the same action, not another post. Do not request a new recommendation to retry a completed or uncertain post.
Human and plain action output append the server's connect_path and intent_url after the error code for reconnect and manual sharing. These fields use the same control-character escaping as the other columns; JSON and MCP retain the original response fields.
MCP exposes marketing_recommend, marketing_approve, marketing_schedule, marketing_cancel, and marketing_status. Unlike other mutations, marketing does not receive an implicit --yes: first show the preview and get approval, then supply {"args":["<action-id>"],"yes":true,"confirmation-token":"<reviewed-token>"}. The token must come from the preview the seller confirmed, not a fresh lookup; missing or changed tokens prevent approve/schedule. The server's X executor handles reconnect and unknown-result states; clients must surface error_code rather than claim success from HTTP 200.
# Check auth (do this first if unsure)
gumroad auth status --json --no-input
# Start device authorization and wait for human approval
gumroad auth login --no-input
# Use an existing seller token without browser approval
gumroad auth login --with-token --json --no-input < token.txt
printf '%s\n' "$GUMROAD_ACCESS_TOKEN" | gumroad auth login --with-token --json --no-input
# Print the active resolved seller token for another tool
gumroad auth token --no-input
# Force the local browser OAuth flow
gumroad auth login --web
# Logout
gumroad auth logout --yes --no-inputBoth login paths retry a token endpoint that answers 408, 429, 500, 502, 503 or 504, waiting at least as long as Retry-After asks. Device login prints one line to stderr when Gumroad starts rate limiting its polls, and keeps polling while the code can still be collected; if the code expires while its polls are rate limited (the last answer was a 429), or a Retry-After runs past the code's expiry, the login ends with the rate limit named rather than a bare authorization expired. With --web, the CLI prints a line to stderr before each wait and keeps retrying until shortly before the authorization code expires; a throttle that outlasts that ends the login with the throttle named rather than a bare HTTP 429. Nothing was stored, so the remedy is to wait and run the login again. Approve promptly, run one login at a time, and never put gumroad auth login in a retry loop: every attempt counts against the same rate limit.
gumroad user --json --no-input
gumroad user --json --jq '.user.email' --no-input
# Update the seller name and/or bio. Pass an empty value to clear a field.
gumroad user update --name "Jane Doe" --bio "I make great things." --json --no-input
gumroad user update --bio "" --json --no-input
# Custom HTML profile landing page (authored by your agent; no checkout flags).
gumroad user page preview ./landing.html --json --no-input
gumroad user page publish ./landing.html --json --no-input
gumroad user page publish - --json --no-input < landing.html
gumroad user page clear --yes --json --no-input
gumroad user page url --no-input
gumroad user page url --json --jq '.profile_url' --no-input# View the current account-level refund policy
gumroad refund-policy view --json --no-input
gumroad refund-policy view --json --jq '.refund_policy.in_effect' --no-input
# Set the refund period. Allowed values: none, 7, 14, 30, 183.
gumroad refund-policy set --period 30 --fine-print "Refund requests are reviewed within 2 business days." --json --no-input
# Clear fine print. This is account-level, not per-product.
gumroad refund-policy set --period none --fine-print "" --json --no-input# Admin commands need internal admin auth.
# In agents/CI, set GUMROAD_ADMIN_TOKEN and pass --non-interactive.
# Inspect user identity, sign-in, social, risk, payout, and watchlist state
# Look up by --email, --user-id, or --username (resolves user_id > email > username)
gumroad admin users info --email [email protected] --json --non-interactive --no-input
gumroad admin users info --username sellerone --json --non-interactive --no-input
# Review affiliate relationships
gumroad admin users affiliates --user-id 2245593582708 --direction granted --limit 50 --json --non-interactive --no-input
gumroad admin users affiliates --username sellerone --direction granted --limit 50 --json --non-interactive --no-input
gumroad admin users affiliates --email [email protected] --direction received --cursor cur-next --json --non-interactive --no-input
# Read and add admin comments
gumroad admin users comments list --user-id 2245593582708 --type note --limit 50 --json --non-interactive --no-input
gumroad admin users comments list --username sellerone --type note --limit 50 --json --non-interactive --no-input
gumroad admin users comments add --user-id 2245593582708 --content "VAT exempt confirmed" --yes --json --non-interactive --no-input
# Account credits. credits add is a high-stakes write: dry-run first, then issue with explicit --yes.
# Amounts are cents, positive only, capped at $1,000 unless --allow-large-amount is explicitly passed.
gumroad admin users credits list --user-id 2245593582708 --limit 50 --json --non-interactive --no-input
gumroad admin users credits list --username sellerone --limit 50 --json --non-interactive --no-input
gumroad admin users credits add --user-id 2245593582708 --expected-email [email protected] --amount-cents 1000 --reason "Goodwill for checkout bug" --dry-run --json --non-interactive --no-input
gumroad admin users credits add --user-id 2245593582708 --expected-email [email protected] --amount-cents 1000 --reason "Goodwill for checkout bug" --yes --json --non-interactive --no-input
# Inspect compliance, Radar risk, and buyer history
gumroad admin users compliance --user-id 2245593582708 --json --non-interactive --no-input
gumroad admin users compliance --username sellerone --json --non-interactive --no-input
gumroad admin users radar --user-id 2245593582708 --limit 50 --json --non-interactive --no-input
gumroad admin users radar --username sellerone --limit 50 --json --non-interactive --no-input
gumroad admin users purchases --user-id 2245593582708 --status successful --has-early-fraud-warning=false --limit 50 --json --non-interactive --no-input
gumroad admin users purchases --username sellerone --status successful --limit 50 --json --non-interactive --no-input
gumroad admin users suspension --username sellerone --json --non-interactive --no-input
# Stored social evidence is optional positive context, never payout approval.
# Check currently_linked and last_verified_at; missing audience counts are unknown.
# Historical SHADOW results describe evaluated_on, never current eligibility or payout authority.
# Missing/null shadow snapshots are not negative scores. --plain remains connection-only;
# use --json or --jq .latest_shadow_evaluation to read the snapshot.
gumroad admin users social-connections --email [email protected] --json --non-interactive --no-input
# --plain columns: platform, uid, handle, currently_linked, last_verified_at,
# account_created_at, follower_count, post_count, last_posted_at, shared_identity_user_count.
# Historical disconnected rows remain evidence, not a live link. This command does not
# refresh providers or authorize payouts; human mode may show a Stored score from a
# historical shadow snapshot only. Use normal identity/risk review before release.
# Find related accounts by risk signals
gumroad admin users related --email [email protected] --signal ip --signal payment_address --json --non-interactive --no-input
gumroad admin users related --username sellerone --signal ip --json --non-interactive --no-input
gumroad admin users related --email [email protected] --json --jq '{related_users, truncated, per_signal_limit}' --non-interactive --no-input
# Mutate user compliance and suspension state.
# mark-compliant never lifts a suspension unless you ask for it. On a suspended
# account a plain mark-compliant is refused with a 422: the server assumes a
# caller that only reviewed the account's finances did not mean to reverse an
# enforcement decision it never looked at. To intentionally restore a suspended
# account, pass --clear-suspension, which is the only thing that sends
# clear_suspension: true.
gumroad admin users mark-compliant --user-id 2245593582708 --expected-email [email protected] --note "Cleared after review" --yes --json --non-interactive --no-input
gumroad admin users mark-compliant --user-id 2245593582708 --expected-email [email protected] --note "Suspension lifted after appeal review" --clear-suspension --yes --json --non-interactive --no-input
gumroad admin users suspend --user-id 2245593582708 --expected-email [email protected] --note "Chargeback risk confirmed" --yes --json --non-interactive --no-input
gumroad admin users suspend-for-tos-violation --user-id 2245593582708 --expected-email [email protected] --note "DMCA takedown notice confirmed" --yes --json --non-interactive --no-input
gumroad admin products flag-for-tos-violation <product-id> --user-id 2245593582708 --expected-email [email protected] --yes --json --non-interactive --no-input
# Hold or release new sales of one product without suspending its seller.
# Needs a per-actor admin token (legacy shared tokens get a 401) and a Gumroad API
# deploy that includes product sales holds (antiwork/gumroad#8667); before that
# deploy both commands fail with a 404.
# A hold unpublishes the listing if live, blocks new checkouts (bundles included)
# and seller republishing; it sends no email and leaves the account, other
# products and existing buyers alone. Holding an already-held product returns the
# existing hold (status already_held). Keep .sales_hold.id: release needs it.
# Read the current hold with: products view <product-id> --json --jq '.product.active_sales_hold'
gumroad admin products view <product-id> --json --jq '.product.active_sales_hold' --non-interactive --no-input
gumroad admin products hold-sales <product-id> --user-id 2245593582708 --expected-email [email protected] --dry-run --json --non-interactive --no-input
gumroad admin products hold-sales <product-id> --user-id 2245593582708 --expected-email [email protected] --yes --json --non-interactive --no-input
# Release only lifts the named --hold-id. Releasing an older hold leaves a newer
# one active: check .product.active_sales_hold_id in the response and do not
# release that newer hold unless a human asked for it. Release never republishes
# the listing; the seller has to publish it again.
gumroad admin products release-sales-hold <product-id> --user-id 2245593582708 --expected-email [email protected] --hold-id <hold-id> --yes --json --non-interactive --no-input
# A timeout, lost connection, 408, 429 or 5xx leaves the result unknown; the CLI
# does not resend. Re-read .product.active_sales_hold before running either command again.
# Feature announcement emails to opted-in accounts. A campaign starts as a draft.
# Show the preview to a human and get their approval before you approve. approve
# takes the content_digest from that preview, so the server refuses it if the
# copy changed after the human saw it. Approval freezes the copy. send starts the
# emails; the server rechecks consent, account state and audience for each one.
gumroad admin announcements create new_feature --audience creators --subject "Try the new feature" --body-file body.html --json --non-interactive --no-input
gumroad admin announcements preview new_feature --json --non-interactive --no-input
gumroad admin announcements approve new_feature --content-digest <digest> --yes --json --non-interactive --no-input
gumroad admin announcements send new_feature --yes --json --non-interactive --no-input
gumroad admin announcements view new_feature --json --non-interactive --no-input
gumroad admin payouts scheduled create --user-id 2245593582708 --expected-email [email protected] --processor stripe --payout-date 2026-06-15 --note "Appeal window closes before payout." --yes --json --non-interactive --no-input
gumroad admin payouts scheduled list --status pending --user-id 2245593582708 --json --non-interactive --no-input
# Recent payouts, with Stripe transfer id and destination bank account per row
gumroad admin payouts list --user-id 2245593582708 --limit 25 --json --jq '.recent_payouts[] | {external_id, stripe_transfer_id, bank_account}' --non-interactive --no-input
# Refund-balance dry-run still calls the preview GET, but skips the guarded POST.
gumroad admin users refund-balance --user-id 2245593582708 --expected-email [email protected] --dry-run --json --non-interactive --no-input
gumroad admin users refund-balance --user-id 2245593582708 --expected-email [email protected] --yes --json --non-interactive --no-input
# Queue fraud refunds for every remaining successful purchase of a suspended seller.
# Requires --expected-email and --expected-count (409 on mismatch or if a run is
# already queued). Buyers are NOT blocked by default; add --block-buyers only for
# buyer-fraud cases (self-purchase / card-testing rings). Refunds run in a
# background job; completion is recorded on the seller's comments and audit log.
gumroad admin users refund-all-for-fraud --user-id 2245593582708 --expected-email [email protected] --expected-count 18 --yes --json --non-interactive --no-input
gumroad admin users refund-all-for-fraud --user-id 2245593582708 --expected-email [email protected] --expected-count 18 --block-buyers --yes --json --non-interactive --no-input
# Inspect purchase and product fraud context
gumroad admin purchases view <purchase-id> --with-clusters --json --non-interactive --no-input
gumroad admin purchases search --email [email protected] --json --jq '{purchases, has_more, limit}' --non-interactive --no-input
gumroad admin purchases lookup --stripe-fingerprint fp_abc --limit 25 --json --non-interactive --no-input
# Refund a purchase. --reason is required: it is stored on the refund and shown to the
# creator in the "A sale has been refunded" notification email.
gumroad admin purchases refund <purchase-id> --email [email protected] --reason "Buyer reported being charged twice" --yes --json --non-interactive --no-input
gumroad admin purchases refund <purchase-id> --email [email protected] --amount 5.00 --reason "Partial refund agreed with buyer" --yes --json --non-interactive --no-input
# Refund only the Gumroad-collected taxes on a purchase, without touching the price.
# --business-vat-id records a buyer-supplied VAT ID so later renewals self-account for
# VAT. --vat-exempt-territory is the equivalent for a buyer in an EU-VAT-exempt territory
# such as the Canary Islands who has no VAT ID to enter: it marks the subscription so
# renewals are charged without VAT, which is the only lasting fix when the buyer's
# checkout connection geolocated to the mainland and the exemption never applied.
gumroad admin purchases refund-taxes <purchase-id> --email [email protected] --yes --json --non-interactive --no-input
gumroad admin purchases refund-taxes <purchase-id> --email [email protected] --business-vat-id GB123456789 --yes --json --non-interactive --no-input
gumroad admin purchases refund-taxes <purchase-id> --email [email protected] --vat-exempt-territory --note "Canary Islands company" --yes --json --non-interactive --no-input
gumroad admin products list --email [email protected] --page 2 --per-page 25 --json --non-interactive --no-input
gumroad admin products view <product-id> --with-fraud-context --json --non-interactive --no-input
gumroad admin products view <product-id> --json --jq '.product.files[] | [.id, .display_name]' --non-interactive --no-input
gumroad admin products files download <product-id> <file-id> --non-interactive --no-input
# Watchlist state does not pause payouts or change user risk state
gumroad admin users watch --user-id 2245593582708 --expected-email [email protected] --revenue-threshold 200 --note "Review next buyers" --yes --json --non-interactive --no-input
gumroad admin users update-watch --user-id 2245593582708 --expected-email [email protected] --revenue-threshold 500 --yes --json --non-interactive --no-input
gumroad admin users update-watch --user-id 2245593582708 --expected-email [email protected] --revenue-threshold 500 --clear-note --yes --json --non-interactive --no-input
gumroad admin users unwatch --user-id 2245593582708 --expected-email [email protected] --yes --json --non-interactive --no-input
# Tell a seller that Stripe paused their connected account under a court order.
# --dry-run asks the server to render the email: it returns the matched seller, the
# notice's delivery status and the complete email, and sends and records nothing.
# A real call records an admin comment keyed by --notice-key and queues the email.
# "queued" (delivery_status pending) means the mail service has not accepted it yet:
# rerun the same command with the same key; each pending call queues the email again.
# "already_sent" (delivery_status accepted, accepted_at set) means the mail service
# accepted it and nothing new is queued. Accepted is not inbox delivery. Confirm the
# notice to Stripe or archive it only after a call answers already_sent.
gumroad admin stripe court-order-notice --stripe-account-id acct_1Example --firm-name "Example Law LLP" --firm-email [email protected] --notice-key <key> --dry-run --json --non-interactive --no-input
gumroad admin stripe court-order-notice --stripe-account-id acct_1Example --firm-name "Example Law LLP" --firm-email [email protected] --notice-key <key> --yes --json --non-interactive --no-input# List products (paginated)
gumroad products list --json --no-input
gumroad products list --all --json --no-input
gumroad products list --page-key <cursor> --json --no-input
# View a product
gumroad products view <id> --json --no-input
# Find product categories
gumroad products categories --search figma --json --no-input
# See what similar products charge before picking a price
gumroad products comps --category design/ui-and-web/figma --json --no-input
gumroad products comps --category music-and-sound-design --query "whoosh sfx" --json --no-input
# Create a product (published immediately; pass --draft to save it unpublished)
gumroad products create --name "Art Pack" --price 10.00 --json --no-input
gumroad products create --name "Art Pack" --price 10.00 --draft --json --no-input
gumroad products create --name "Figma Kit" --category design/ui-and-web/figma --json --no-input
gumroad products create --name "Art Pack" --price 10.00 --file ./pack.zip --file-name "Art Pack.zip" --json --no-input
gumroad products create --name "Art Pack" --price 10.00 --cover-image ./cover.jpg --thumbnail ./thumb.jpg --json --no-input
gumroad products create --name "Newsletter" --type membership --subscription-duration monthly --json --no-input
gumroad products create --name "E-Book" --type ebook --price 5 --tag art --tag digital --json --no-input
# Update a product
gumroad products update <id> --name "New Name" --json --no-input
gumroad products update <id> --price 15.00 --currency eur --json --no-input
gumroad products update <id> --category design/ui-and-web/figma --json --no-input
gumroad products update <id> --file ./pack.zip --json --no-input
gumroad products update <id> --file ./lesson-1.mp4 --file ./lesson-2.pdf --page <page_id> --json --no-input
gumroad products update <id> --cover-image ./cover.jpg --json --no-input
gumroad products update <id> --preview-image ./gallery-1.jpg --preview-image ./gallery-2.jpg --json --no-input
gumroad products update <id> --preview-video ./demo.mp4 --json --no-input
gumroad products update <id> --thumbnail ./thumb.jpg --json --no-input
# Product-level refund policy override (only when the account-level policy is off).
# Allowed periods: inherit, none, 7, 14, 30, 183. "inherit" returns to the account default.
gumroad products update <id> --refund-period none --refund-fine-print "No refunds once downloaded." --json --no-input
gumroad products update <id> --refund-period inherit --json --no-input
gumroad products create --name "Art Pack" --price 10 --refund-period none --json --no-input
gumroad products page preview <id> ./landing.html --json --no-input
gumroad products page publish <id> ./landing.html --json --no-input
gumroad products page publish <id> - --json --no-input < landing.html
gumroad products page clear <id> --yes --json --no-input
gumroad products page url <id> --no-input
gumroad products page url <id> --json --jq '.product.landing_url' --no-input
# Product covers and thumbnail
gumroad products covers add <id> --image ./cover.jpg --json --no-input
gumroad products covers add <id> --url https://www.youtube.com/watch?v=qKebcV1jv3A --json --no-input
gumroad products covers reorder <id> <cover_id> <cover_id> --json --no-input
gumroad products covers remove <id> <cover_id> --yes --json --no-input
gumroad products thumbnail set <id> --image ./thumb.jpg --json --no-input
gumroad products thumbnail set <id> --url https://example.com/thumb.png --json --no-input
gumroad products thumbnail remove <id> --yes --json --no-input
# Shared product rich content. `set` replaces the whole page array; dry-run first.
gumroad products content list <id> --json --no-input
gumroad products content get <id> --json --no-input > content.json
gumroad products content set <id> content.json --dry-run --json --no-input
gumroad products content set <id> content.json --yes --json --no-input
gumroad products content get <id> --page <page_id> --json --no-input > page.json
gumroad products content set <id> --page <page_id> --dry-run --json --no-input
gumroad products content set <id> page.json --page <page_id> --dry-run --json --no-input
# Upload a whole course: every file in the manifest, one product write per page.
gumroad products content upload <id> course.json --dry-run --json --no-input
gumroad products content upload <id> course.json --json --no-input
gumroad products content upload <id> course.json --state ./my-course.state.json --json --no-input
# Per-variant rich content. `--category` is required because the variant endpoint is category-scoped.
gumroad products content list <id> --variant <variant_id> --category <cat_id> --json --no-input
gumroad products content get <id> --variant <variant_id> --category <cat_id> --json --no-input > content.json
gumroad products content set <id> content.json --variant <variant_id> --category <cat_id> --dry-run --json --no-input
gumroad products content set <id> content.json --variant <variant_id> --category <cat_id> --yes --json --no-input
# Bundles: create with --type bundle, then set its contents (whole-list replace, order = position)
gumroad products create --name "Starter Bundle" --type bundle --price 29 --json --no-input
gumroad products bundle set <bundle_id> --product <id_a> --product <id_b> --dry-run --json --no-input
gumroad products bundle set <bundle_id> --product <id_a> --variant <id_a>=<variant_id> --quantity <id_a>=2 --json --no-input
gumroad products bundle list <bundle_id> --json --no-input
# Publish / unpublish
gumroad products publish <id> --json --no-input
gumroad products unpublish <id> --json --no-input
# Delete (destructive — needs --yes)
gumroad products delete <id> --yes --json --no-input
# List SKUs for a product
gumroad products skus <id> --json --no-input
In custom HTML, use Gumroad data attributes for live product values and checkout:
<h1 data-gumroad-field="name">Product name</h1>
<span data-gumroad-field="price">$0</span>
<p data-gumroad-field="description">Product description</p>
<a data-gumroad-action="buy">Buy now</a>
<a data-gumroad-action="buy" data-gumroad-option="Pro" data-gumroad-recurrence="yearly">Buy Pro - $99/year</a>
<button data-gumroad-action="buy" data-gumroad-quantity="2">Buy 2 seats</button>
<button data-gumroad-action="buy" data-gumroad-price="19.99">Pay $19.99</button>List flags: --all, --page-key.
Categories: products categories [--search <term>] returns label, path, and numeric ID. Prefer --category <path> for product create/update. --taxonomy-id remains supported when you already have the numeric ID, but it cannot be combined with --category.
Comps: products comps [--category <path>] [--query <text>] [--currency <iso>] (category or query required) reports the price distribution of comparable priced public products: count of discoverable priced listings (free products excluded), p25/p50/p75 of price_cents in one currency (default usd), and the top five products by sales volume with name, formatted price, and public URL. --category includes descendant categories. Use it to anchor pricing advice in real marketplace numbers instead of static defaults.
Create flags: --name (required), --price, --type (digital|course|ebook|membership|bundle|coffee|call|commission), --currency, --pay-what-you-want, --suggested-price, --description, --custom-summary, --custom-permalink, --custom-receipt, --max-purchase-count, --category, --taxonomy-id, --tag (repeatable), --file (repeatable), --file-name (repeatable, aligned to --file), --file-description (repeatable, aligned to --file), --cover-image, --preview-image (repeatable), --preview-video (repeatable), --thumbnail.
Bundles: products create --type bundle makes an empty bundle; products update cannot change its contents. Use products bundle set <bundle_id> --product <id> [--product <id> ...] (--variant <product_id>=<variant_id>, --quantity <product_id>=<n>) to replace the full list of bundled products, and products bundle list <bundle_id> to read it back. Products left out of set are removed from the bundle.
Update flags: --name, --price, --currency, --description, --custom-summary, --custom-permalink, --custom-receipt, --max-purchase-count, --category, --taxonomy-id, --tag (repeatable), --custom-html, --file (repeatable), --file-name, --file-description, --page, --cover-image, --preview-image (repeatable), --preview-video (repeatable), --thumbnail. Prefer products page preview/publish/clear/url for custom HTML page workflows; products update --custom-html remains supported as a low-level product update flag.
Use products update --file for shared product Content. It appends each new file embed to the existing page and leaves prior embeds intact. Without --page, every upload lands on the last page that already holds files. Pass --page <page_id> (from gumroad products content list) to place the whole batch on one named page in a single product write, so a multi-page course costs one write per page instead of one per file; an unknown page id fails before any upload starts. Use products content get/set for structural content edits. For products with per-variant Content, use variants update ... --file for the specific variant you want to change.
Use --cover-image for the primary cover, repeat --preview-image for additional gallery/preview images, repeat --preview-video for video previews (MP4, MOV, M4V, MPEG, WMV, or WebM), and --thumbnail for the card/library thumbnail. When multiple media flags are combined, covers attach in a fixed order: cover image first, then preview images (in flag order), then preview videos (in flag order) — images and videos cannot be interleaved in a single command. These media flags run the required two-step API flow: direct upload first, then attach by signed blob ID. For an existing product, products thumbnail set --url asks Gumroad to download and attach a public HTTP(S) image directly.
# Upload a file and print the canonical Gumroad URL
gumroad files upload ./pack.zip --json --no-input
gumroad files upload ./pack.zip --name "Art Pack.zip" --json --no-input
# Finalize a saved recovery manifest after a state-unknown upload
gumroad files complete --recovery recovery.json --yes --json --no-input
jq '.error.recovery' err.json | gumroad files complete --recovery - --yes --json --no-input
# Abort an orphaned multipart upload
gumroad files abort --upload-id up-123 --key attachments/u/k/original/pack.zip --yes --json --no-inputfiles upload and files complete both return .file_url. When a JSON upload fails with recovery details, reuse .error.recovery with files complete to finish it or files abort to reclaim the orphaned multipart upload.
gumroad builds deploy <dir|zip> --product <id> [--promote] uploads a build on the multipart path and records it as a new immutable build. A directory is zipped for you and must have a regular index.html file at its top level; dotfiles and dot-folders are skipped. The directory may itself be a symlink, but links inside it are not followed: deploy fails on one instead of leaving the file out, so copy the real file in or deploy a .zip. gumroad builds list --product <id> shows builds (.builds[]: .id, .number, .status, .byte_size); gumroad builds promote <build-id> --product <id> makes a build live (superseding the previous one; promoting an older build rolls back). A deployed build is not live until promoted, and the live build is attached to the product as its download. builds deploy --json wraps the API response in the mutation envelope, so read the build at .result.build (.id, .number, .status).
Files from files upload are stored privately and can never be displayed on custom product landing pages or profile pages — the pages' Content-Security-Policy only allows Gumroad's public CDN, and page moderation cannot fetch private URLs, so a page embedding one fails review every time. To put an image on a page, upload it to the public media library instead and embed the returned .media.url in the page HTML before pages push / products page publish.
# Host an image publicly and print its page-embeddable URL
gumroad media upload ./logo.png --json --no-input
gumroad media upload ./logo.png --name "Store logo" --json --jq '.media.url' --no-input
# List and delete media library files
gumroad media list --json --no-input
gumroad media delete k3n8xq1p9wr2sd4a --yes --json --no-inputThe CLI detects JPEG, PNG, GIF, WebP, BMP, and ICO images up to 10 MB. It rejects SVG and other formats it cannot identify locally. Gumroad checks each image again and moderates it before hosting. A flagged image fails with the moderation message. Deleting a file breaks each page that still embeds its URL. The upload command requires the edit_profile scope. The list command requires the view_profile scope. Tokens created before the CLI requested these scopes fail with Access denied: This endpoint requires the view_profile scope. Run gumroad auth login again to create a token with the new scopes.
If an upload returns media_commit_state_unknown, do not retry it automatically. Read .error.recovery.key. List the media and find the item whose URL contains that key. If the item exists, the upload completed. If it does not exist, keep .error.recovery.signed_blob_id and .error.recovery.key for support. If an upload returns media_direct_upload_state_unknown, do not start a new upload. Keep the same recovery values for support. If it returns media_output_failed, the upload completed. Use .error.recovery.media_id and .error.recovery.media_url. Do not retry the upload. If a delete returns media_delete_output_failed, the deletion completed. Do not retry it. If a delete returns media_delete_state_unknown, list the media and search for .error.recovery.media_id. Do not retry the deletion automatically.
# Create a draft from an HTML body file (or - for stdin). Draft is the default safety behavior.
gumroad emails create --subject "New release" --body ./email.html --json --no-input
gumroad emails create --subject "Product update" --body ./email.html --audience product --product <id> --json --no-input
# Preview before sending; use `.preview_url` for human review.
gumroad emails send-preview <id> --json --jq '.preview_url' --no-input
gumroad emails view <id> --json --no-input
# List drafts, scheduled emails, or sent emails.
gumroad emails list --state draft --json --no-input
gumroad emails list --state published --all --json --no-input
# Send, schedule, unschedule, or delete. Confirmation-gated verbs require --yes.
gumroad emails send <id> --yes --json --no-input
gumroad emails schedule <id> --at "2026-06-18T14:00:00Z" --json --no-input
gumroad emails unschedule <id> --json --no-input
gumroad emails delete <id> --yes --json --no-inputCreate flags: --subject (required), --body (required HTML file path, or - for stdin), --audience (all|customers|followers|product, default all), --product (required for product audience), --send (publish and send immediately). List flags: --state (published|scheduled|draft), --all, --page-key. Schedule flags: --at (required; RFC3339 e.g. 2026-06-18T14:00:00Z, or a naive seller-timezone time e.g. 2026-06-18 14:00 which the API resolves in the seller's timezone). --to-be-published-at is an alias for --at.
Use --dry-run --json --no-input to inspect create params without calling the API. Passing --send blasts the audience immediately; prefer the draft → send-preview URL → send workflow. send-preview emails a copy to the seller. Schedule a draft with emails schedule <id> --at <timestamp> (re-running with a new --at reschedules); emails unschedule <id> returns a scheduled email to draft.
# List workflows with audience, state, and email step count.
gumroad workflows list --json --no-input
# View one workflow's email steps with per-step delay, sent, open, and click stats.
gumroad workflows view <id> --json --no-input
# Compare steps: pull subject and click rate for every step.
gumroad workflows view <id> --json --jq '.workflow.emails[] | {subject, click_rate}' --no-input
# Preview and then add one step from an HTML body file.
gumroad workflows add-email <workflow-id> --subject "Week four" --body ./email.html --delay "4 weeks" --dry-run --json --no-input
gumroad workflows add-email <workflow-id> --subject "Week four" --body ./email.html --delay "4 weeks" --yes --json --no-input
# Update only the supplied fields on one step.
gumroad workflows update-email <workflow-id> <email-id> --body ./email.html --dry-run --json --no-input
gumroad workflows update-email <workflow-id> <email-id> --body ./email.html --json --no-inputadd-email requires --subject, --body, and --delay. update-email requires at least one of these flags. --body accepts an HTML file path or - for stdin. --delay accepts a non-negative integer and hour, day, week, or month; singular and plural units work.
Neither write command changes the workflow publication state. A new step inherits that state. A published workflow can schedule eligible past recipients when you add a step. A delay change can reschedule recipients. add-email and delay updates require --yes when you pass --no-input. Abandoned cart workflows reject added steps and delay changes. Use the dashboard to create, publish, or unpublish a workflow.
If a write returns workflow_email_add_state_unknown or workflow_email_update_state_unknown, do not retry it automatically. Run gumroad workflows view <workflow-id> --json --no-input and inspect step IDs and timestamps. A matching step does not prove an uncertain add completed. Contact support if the state remains unclear. If the command returns workflow_email_output_failed, the write completed. Use .error.recovery.email_id to find the saved step. Do not retry it.
view returns steps ordered by delay, each with delay (amount + unit), sent_count, open_count, open_rate, click_count, and click_rate. Rates are null for unsent steps. Plain output for view prints one row per email step.
# List sales (paginated)
gumroad sales list --json --no-input
gumroad sales list --product <id> --after 2024-01-01 --json --no-input
gumroad sales list --email [email protected] --json --no-input
gumroad sales list --all --json --no-input
gumroad sales list --after 2024-01-01 --before 2024-01-31 --csv --no-input
# Find a sale by email
gumroad sales list --json --jq '.sales[] | select(.email == "[email protected]")' --no-input
# Deduplicated buyer list for one or more products.
# JSON/CSV/plain include buyer-level last-touch UTM fields; same-timestamp ties keep the first API result.
# Use sales export for the full per-sale web CSV.
gumroad sales buyers --product <id> --json --no-input
gumroad sales buyers --product <old-id> --product <new-id> --json --no-input
gumroad sales buyers --product <id> --after 2024-01-01 --csv --no-input
gumroad sales buyers --product <id> --json --jq '.buyers[].email' --no-input
# Show sales totals
gumroad sales summary --json --no-input
gumroad sales summary --from 2026-01-01 --to 2026-05-21 --json --no-input
gumroad sales summary --group-by product --json --no-input
gumroad sales summary --group-by month --from 2026-01-01 --to 2026-05-21 --json --no-input
# Request an emailed CSV export for larger ranges
gumroad sales export --from 2026-01-01 --to 2026-05-21 --no-input
gumroad sales export --after 2026-01-01 --before 2026-05-21 --no-input
gumroad sales export --product <id> --json --no-input
# View a sale
gumroad sales view <id> --json --no-input
# Refund (destructive — needs --yes)
# --amount is in the sale's OWN currency, so a partial refund fetches the sale
# first to read that currency. That extra GET is why a yen-priced sale accepts
# `--amount 25` as ¥25: without it, 25 would be scaled to ¥2500. A full refund
# sends no amount and makes no lookup. If a token has refund permission but lacks
# view_sales, pass --currency explicitly; the CLI then trusts that currency and
# skips the lookup.
gumroad sales refund <id> --yes --json --no-input
gumroad sales refund <id> --amount 5.00 --yes --json --no-input
gumroad sales refund <id> --amount 25 --currency jpy --yes --json --no-input
# A sale whose currency cannot be read is refused rather than guessed:
# re-run without --amount for a full refund, or pass --currency if you already
# know the sale's listed currency.
# Resend receipt
gumroad sales resend-receipt <id> --json --no-inputList filters/output: --product, --order, --email, --after (YYYY-MM-DD), --before (YYYY-MM-DD), --all, --page-key, --csv. Buyer-currency amounts: sales charged in the buyer's local currency carry a buyer_presentment object in JSON output (currency, price_cents, tip_cents, tax fields, shipping_cents, total_cents, fx_rate, refunded_cents); it is omitted for canonical (USD-charged) sales. The list table appends (buyer: 21.01 CAD) to TOTAL, sales list --csv has four trailing columns (buyer_currency, buyer_total_cents, buyer_refunded_cents, buyer_fx_rate — empty for canonical sales), sales view prints Buyer charged: and FX rate: lines, and sales view --plain appends one extra column ONLY for presentment sales (canonical sales keep the seven-column schema). Summary filters: --from (YYYY-MM-DD), --to (YYYY-MM-DD), --group-by (product|day|week|month). Export filters: --from/--after (YYYY-MM-DD), --to/--before (YYYY-MM-DD), --product.
# List payouts
gumroad payouts list --json --no-input
gumroad payouts list --after 2024-01-01 --before 2024-12-31 --json --no-input
gumroad payouts list --all --json --no-input
# View a payout
gumroad payouts view <id> --json --no-input
gumroad payouts view <id> --include-transactions --json --no-input
# Upcoming payout
gumroad payouts upcoming --json --no-inputList filters: --after, --before, --all, --page-key, --no-upcoming. View flags: --include-transactions, --no-sales.
gumroad subscribers list --product <id> --json --no-input
gumroad subscribers list --product <id> --email [email protected] --json --no-input
gumroad subscribers list --product <id> --all --json --no-input
gumroad subscribers view <id> --json --no-inputList flags: --product (required), --email, --all, --page-key.
License keys are passed via stdin. Never pass keys as command-line arguments.
# Verify without incrementing use count
echo "$LICENSE_KEY" | gumroad licenses verify --product <id> --no-increment --json --no-input
# Verify (increments use count)
echo "$LICENSE_KEY" | gumroad licenses verify --product <id> --json --no-input
# Enable / disable
echo "$LICENSE_KEY" | gumroad licenses enable --product <id> --json --no-input
echo "$LICENSE_KEY" | gumroad licenses disable --product <id> --json --no-input
# Decrement use count
echo "$LICENSE_KEY" | gumroad licenses decrement --product <id> --json --no-in~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.