CLI

Command reference

Every Zanfia CLI command, grouped by resource in the order the CLI registers them: auth, products, offers, checkout settings, site, emails, OTO, bumps, courses, certificates, consultations, newsletters, communities, conversations, clients, campaigns, segments, forms, testimonials, brand, team, orders, subscriptions, stats, discounts, referrals, workflows, funnels, pages, social, media.

25 min readLast updated Nov 12, 2026

All commands support --json for machine-readable output; --api-key <key> overrides the stored credential for one call and --verbose logs request/response details. These global flags work at any position (zanfia --json products list and zanfia products list --json are equivalent). Money amounts are minor units (1999 = 19.99) unless a flag says otherwise. Every command maps onto a Workspace API route, so anything here also works as raw HTTP. Every leaf command is listed below: one row each; <command> --help prints the full flag list, and <command> --help --short prints only the command names (subcommands indented, no descriptions): handy for a quick map of a group or of the whole CLI (zanfia --help --short).

JSON-taking options and the --json output flag

Wherever a command takes a JSON value (--json, --patch, --config, --chapters, …) both --json <file|inline> and --json=<file|inline> work. Every such option accepts inline JSON or a path to a .json file. On commands where --json carries a request body, switch to machine-readable output with -o json (or put the global flag before the command: zanfia --json offers update <id> --json body.json).

Auth

  • zanfia login [--workspace <id|slug>] [--address <creator-platform-url>] [--api-url <url>]
    Browser sign-in (OAuth consent page): pick the workspace there (Owner or Admin; --workspace preselects it) and the CLI stores a 90-day token for it, which becomes the active login. Logins of other workspaces stay stored. --address signs in as a community member of that creator's platform: Community API only
  • zanfia workspace list
    The workspace logins stored on this device; * marks the active one
  • zanfia workspace use <id|slug>
    Make another stored workspace login the active one, without signing in again
  • --workspace <id|slug> on any command
    Use another stored workspace login for that one command (--api-key and ZANFIA_API_KEY still win)
  • zanfia whoami
    The active workspace and identity, and where the key comes from (same as auth whoami)
  • zanfia register [--email <e>] [--name <n>] [--lang en|pl] [--access-code <c>] [--code <c>]
    Create a new creator account and log the CLI in: opens the signup page in your browser (a quick security check), then logs in like zanfia login
  • zanfia join <handle> [--email <e>] [--name <n>] [--code <c>]
    Join a public community from the terminal (emailed code) and save a member key: no account or browser needed
  • zanfia logout [--all]
    Forget the active login (the newest remaining one becomes active); --all forgets every stored login
  • zanfia auth status
    Show what the CLI has stored: path, credential source, expiry, API URL
  • zanfia auth set-key <key> [--api-url <url>]
    Store an API key as the credential (member keys: pass the creator's api-community URL)
  • zanfia auth whoami
    The workspace (or community member) identity behind the current credential

Products

  • products list
    List products in the workspace
  • products get <id>
    One product
  • products create --name <n> --type <course|newsletter|community|…> [--description | --description-file <path>] [--free | --amount <major> --currency <c> [--period one_time|monthly|yearly]] [--cover <pathOrUrl>] [--file <path>…] [--language pl|en]
    New product; community/newsletter types seed their full content. --free / --amount also seed a default offer (amount in major units here, e.g. 49.99). One-shot extras: --cover sets the cover image (a local file is uploaded to the media library first; an https URL is used as-is), --file (repeatable) uploads local files and attaches them to a downloads product, --description-file reads the description from a file, --language pl|en sets the product's own language (buyer-facing pages and e-mails; omitted = the platform's language) and picks the --amount currency when --currency is omitted (pl → pln, else usd)
  • products update <id> [--name] [--description] [--visibility <public|unlisted|private>] [--language pl|en|none] [--buyer-files on|off]
    Rename / re-describe; --visibility sets where buyers find it once published: public (store + product page), unlisted (product page by link only), private (checkout links only); --language pl|en|none sets the product's language (none clears it: the platform's language applies); --buyer-files on|off sets "Files for buyers" (the product's file set from files push open to its buyers, or hidden; the files stay saved)
  • products publish <id>
    Status → published (live for customers)
  • products draft <id>
    Status → draft (hidden from customers)
  • products archive <id>
    Status → archived (not a delete)
  • products duplicate <id> [--name <name>] --confirm
    Copy a product as a draft (settings, offers with new prices, content) in a background run; --name names the copy (default: the name + "(copy)"). Community, knowledge base and newsletter products are refused (422)
  • products prices list <pid>
    Prices collected across the product's checkouts
  • products prices create <pid> --amount <minor> --currency pln|usd|eur|gbp [--label]
    Add a one-time paid price (creates a default checkout if needed; gateway sync is server-side)
  • products appointment get <pid>
    Appointment config of a consultation / service / event product
  • products appointment set <pid> [--config=<json|file>] [--booking-method native|external] [--booking-url] [--duration <min>] [--location] [--deliverable …] [--delivery-time <n> --delivery-unit days|weeks] [--revision-rounds <n|unlimited>] [--brief-method] [--brief-url] [--event-mode online|offline] [--starts <iso>] [--ends <iso>] [--capacity <n>] [--show-on-success on|off] [--booking-questions <json|file>]
    Update the appointment config: reads the current one first, so partial flags are fine; a first-time setup needs every required field (or --config). --booking-questions (consultations) REPLACES the booking-window questions with a JSON array of { id?, label, type: shortText|longText|phone|date, required? }: keep the ids from appointment get --json, a question without one is new; [] removes them all
  • products sales-page get <pid>
    The page the product's address shows (its Product page), or the themed product page
  • products sales-page set <pid> --page <pageId> | --themed
    Make a PUBLISHED page of the workspace the product page, or put the themed page back (key with products:write + pages:read)
  • products after-purchase get <pid>
    Thank-you page + email settings
  • products after-purchase set <pid> [--settings=<json|file>] [--thank-you-type defaultPage|redirect] [--redirect-url] [--email-buyer-purchase on|off] [--notify-owner-purchase on|off] [--email-buyer-failed on|off] [--notify-owner-failed on|off]
    Update after-purchase settings (partial flags merge); --settings for the full object with custom thank-you / email content
  • products images set <pid> --image <url> [--image <url> …]
    Replace the cover images (full replace; https URLs already hosted: upload to the media library first)

Plans and bundling (products plans)

Tiers with included products. Invariants are server-enforced: unique plan ids, at most one default, one level of bundling, and a plan still sold by active prices cannot be removed.

  • products plans get <pid>
    The product's plans: id, name, active, default, included products
  • products plans set <pid> --json <file|inline>
    Full replace of the plans array (or {plans: [...]}): send every plan you keep, with its id; plans without an id are created
  • products plans count-clients <pid> [--plan <planId>]
    Active customers whose effective plan is --plan (omit it to size a not-yet-saved first/default plan)
  • products plans set-client-plan <pid> --plan <planId> (--client <wcid> | --email <e>)
    Move one client's grant to another plan: billing-neutral; community membership is reconciled server-side
  • products plans backfill <pid> --community <communityProductId> --confirm
    Eagerly create community member docs for every active customer whose plan includes the community

Downloads content (products files)

  • products files get --product <pid>
    Description presence + file table of a downloads product
  • products files set-content --product <pid> (--content <md> | --content-file <path>)
    Replace the Markdown description shown above the file list
  • products files add --product <pid> (--url <url> --name <n> [--media-id] [--size] | --files=<json|file>)
    Attach one file, or several via a JSON array of {url, name, mediaId?, size?}; already-attached mediaIds are skipped
  • products files remove <uid> --product <pid>
    Detach a file by its uid
  • products files reorder <uid...> --product <pid>
    Reorder the file list: pass every current uid in the new order

Knowledge base (products kb)

Address a knowledge-base product by its product id. Writes are committed edits: they also clear the dashboard editor's unpublished draft for the touched article.

  • products kb get <pid>
    Categories (tree keys = the --category ids) and every article, drafts included
  • products kb tree set <pid> --json <file|inline>
    Replace the whole order: {"categories": [{"categoryId", "articleIds": []}, …]}, every category and article; a concurrent edit is refused (tree-changed); re-fetch with products kb get --json and retry
  • products kb article get <pid> <aid>
    One article, published fields, full body
  • products kb article create <pid> --title <t> --category <cid> [--content | --content-file] [--status draft|available|unavailable]
    New article (default draft)
  • products kb article update <pid> <aid> [--title] [--status] [--category <cid>] [--content | --content-file] [--cover <mediaId>:<url> | --clear-cover]
    Edit an article; --cover sets the cover image from the media library (:<url> for an external image)
  • products kb article delete <pid> <aid> --confirm
    Permanent. To hide instead, set --status unavailable
  • products kb category add <pid> --name <n>
    New root category
  • products kb category rename <pid> <cid> --name <n>
    Rename (the articles' denormalized category name follows)
  • products kb category delete <pid> <cid> [--delete-articles] --confirm
    Permanent; a non-empty category needs --delete-articles

Buyer notifications (products notifications)

A short message (a title, up to 200 characters and an optional link on your own platform) to everyone with access to one product right now: a push in the mobile app, a bell item on the portal and, with --send-email, an e-mail. Run preview first: it sends nothing and shows how many people the message reaches; pass that count to send as --confirmed-recipient-count (more people with access by then = 409, preview again). One send per product per hour and a daily cap per workspace (429); a send cannot be recalled, and list shows the delivery counts a little later.

CommandPurpose
products notifications preview <productId> [--link <value>] [--send-email|--no-send-email]Preview a notification to a product's buyers
products notifications send <productId> --title <value> --body <value> [--link <value>] [--send-email] --confirmed-recipient-count <n> [--send-id <value>] --confirmNotify a product's buyers
products notifications list <productId> [--limit <n>]List a product's buyer notifications

Offers (checkouts)

An offer is a checkout of a product; <cid> comes from offers list.

  • offers list [--product <pid>]
    Browse offers
  • offers get <cid> [--full]
    One offer with its prices and attached bumps; --full adds every editable setting (media gallery, theme, deadline, afterPurchase, oto chain, form, legal, invoicing, notifications, tracking): the same keys offers update --json accepts, so you can read, edit and write back
  • offers create --product <pid> [--free] [--name <n>]
    New offer (free offers carry no prices)
  • offers update <cid> [--name] [--slug] [--title] [--description] [--success-url <url|none>] [--language <code|none>] [--show-testimonials on|off] [--show-plan-includes on|off] [--invoice-as-option on|off] [--hide-country-field on|off] [--show-phone-field off|optional|required] [--allow-promotion-code off|subscriptions|all] [--force-b2b on|off] [--show-express-checkout on|off] [--promote-net-prices on|off] [--stripe-tax-behavior inclusive|exclusive] [--multiple-purchases on|off] [--allow-plan-upgrade on|off] [--terms-url] [--privacy-policy-url] [--claim] [--disable-invoice-issuing on|off] [--disable-purchase-email-client on|off] [--tracking-integration-ids <ids|none>] [--json <patch>] …
    Edit any checkout-builder setting: display, form & UX, legal, invoicing, notifications, tracking (--help lists all ~50 flags; booleans take on|off, none clears). Deep fields (theme, deadline, afterPurchase, paymentMethodsDisplay, checkoutMedia, oto chain …) go through --json; explicit flags win
  • offers publish <cid>
    Activate (paid offers need a connected payment gateway)
  • offers unpublish <cid>
    Pause: stops new purchases
  • offers duplicate <cid> [--with-prices] [--name <n>]
    Copy an offer
  • offers notify-waitlist <cid> [--send --confirm]
    "It's back": e-mail the offer's waiting list, once per sign-up. Without --send it only counts
  • offers set-default <cid>
    Make it the product's default checkout
  • offers delete <cid>
    Permanent: prices are archived on the gateways and the offer detached; prefer offers unpublish
  • offers recovery preview <cid> <sessionId>
    Who a cart-recovery email would go to and what it would say. Nothing is sent
  • offers recovery send <cid> <sessionId> --confirm
    Sends a real "finish your purchase" email to the visitor who abandoned the session. Preview first
  • offers price add <cid> --type one_time|recurring|installments --amount <minor> --currency pln|usd|eur|gbp [--interval day|week|month|year] [--interval-count] [--cycles <n>] [--allow-cancellation] [--trial-days] [--label] [--invoice-name] [--hidden] [--description] [--sale-ends-at <iso> | --sale-days <n>] [--quantity-limit] [--show-available] [--compare-at <minor>] [--access-interval --access-count | --access-until <iso>] [--plan <planId>]
    Add a price variant
  • offers price update <cid> <priceId> [--operation activate|deactivate|hide|unhide|change-amount|edit-details] [--amount <minor>] [--label <text>] [--description <markdown>]
    Mutate a price. --label / --description edit the display label and the markdown description under the price (edit-details, implied when --operation is omitted; an empty --description "" clears it); they cannot be combined with another operation
  • offers price delete <cid> <priceId> [--remove]
    Archive (default) or remove entirely
  • offers price reorder <cid> --order id1,id2,…
    Display order: pass all price ids
  • offers media list <cid>
    The offer's media gallery (checkout images and videos) in display order: index 0 is featured
  • offers media add <cid> (--media <mediaId> | --url <url> --type image|video [--thumbnail <url>])
    Append a Media Library item (videos must be ready; the Bunny embed URL is built for you) or an external URL to the gallery: read-modify-write through GET ?include=full + PATCH, validated by the same editor schema as offers update --json
  • offers media remove <cid> <mediaId|url>
    Drop a gallery entry by its media id or URL

Checkout settings

Workspace-wide defaults every checkout page reads unless an offer overrides a field itself.

  • checkout-settings get [--lang pl]
    The workspace cart/checkout page settings
  • checkout-settings update [--lang pl] [--terms-url] [--privacy-policy-url] [--claim] [--terms-content] [--privacy-policy-content] [--google-analytics-id] [--facebook-pixel-id] [--gtm-id] [--tiktok-pixel-id] [--promote-net-prices on|off] [--price-variants-placement belowDescription|aboveDescription|none] [--json <patch>]
    Legal links / texts, tracking ids, net prices; deep fields (embeddedForm, checkoutTheme, cartTheme) via --json; none clears a nullable field
  • checkout-settings consents list
    The consent checkboxes buyers see on checkout pages, in display order
  • checkout-settings consents upsert <name> --label <t> --required|--optional --apply-to all|'<json>' [--url <url|none>] [--marketing-opt-in|--no-marketing-opt-in] [--json <body>]
    Add or edit one consent by its stable name: scope ids are verified against real products/offers (unknown id → 404)

Stripe Tax

The workspace's Stripe Tax settings (Integrations → Payments → Stripe): on / off, the default price mode (inclusive = gross prices, exclusive = net prices with tax added at checkout) and "EU companies pay the net price". Turning it on needs Stripe Tax active in your own Stripe account first. Needs a key with the integrations:read / integrations:write scopes.

CommandPurpose
tax getRead the Stripe Tax settings
tax set [--enabled|--no-enabled] [--behavior <inclusive|exclusive>] [--eu-companies-pay-net|--no-eu-companies-pay-net]Change the Stripe Tax settings

Integrations (conversion tracking)

Managed conversion-tracking integrations (Settings → Integrations → Analytics): Google Tag Manager, Google Analytics 4, TikTok, X, LinkedIn, Microsoft Clarity, Hotjar. Platform aliases: gtm, ga4, clarity, twitter. Assign per offer with offers update --tracking-integration-ids, or workspace-wide with integrations set-defaults. Writes need products:write. The credential-bearing connections below (status, email-clients, meta-capi, manual-payment) need a key with the integrations:read / integrations:write scopes; OAuth providers (Stripe, Meta OAuth, Google, social) and the credential payment gateways stay dashboard-only.

  • integrations list [--client <platform>]
    Every managed integration with its id, platform id, server-side flag and whether it is a workspace default, plus the default id list
  • integrations add <platform> --name <n> (--container-id | --measurement-id [--api-secret] | --pixel-id [--access-token] | --partner-id [--access-token] | --project-id | --site-id) [--purchase-tag] [--init-checkout-tag] [--add-to-cart-tag] [--trial-started-tag] [--track-purchase on|off] [--track-init-checkout on|off] [--track-add-to-cart on|off] [--track-trial-started on|off]
    New integration: ids are pattern-checked and GA4 / TikTok / LinkedIn secrets are verified against the platform before saving; the first integration a workspace connects becomes the workspace default automatically
  • integrations update <platform> <integrationId> [same flags as add]
    Edit: only the flags you pass change; omitted secrets stay stored (--api-secret none / --access-token none = client-side only)
  • integrations remove <platform> <integrationId> --confirm
    Irreversible: offers and defaults still listing the id simply stop tracking on that platform
  • integrations set-defaults <ids|none>
    Replace the workspace-default integrations (comma-separated ids from integrations list or stats conversion-health; none clears) applied to every offer that inherits defaults
  • integrations status
    What is connected: email clients, Meta pixel / Conversions API entries, invoicing, payment methods (needs integrations:read)
  • integrations email-clients connect <ac|kit|mailerlite|mailchimp|getresponse|pipedrive> --url <url> (--api-key <key> | --api-key-stdin | --api-key-env VAR) [--server-prefix <us21>] [--tags-field <f> --lists-field <f>]
    Connect (or re-connect) an email-marketing account: the key is verified against the provider, lists + tags fetched, stored in Secret Manager and never printed; prefer -stdin / -env to keep it out of shell history (integrations:write)
  • integrations email-clients disconnect <client> --url <url> --confirm
    Irreversible: refused while a live newsletter / automation still uses the account (the error lists the products)
  • integrations email-clients refresh <client> --url <url> [--server-prefix]
    Re-fetch the lists + tags with the stored key (not Pipedrive)
  • integrations meta-capi list
    Manual Meta pixel / Conversions API entries
  • integrations meta-capi add --name <n> --pixel-id <id> [--access-token <t> | --access-token-stdin | --access-token-env VAR] [--track-purchase on|off] [--track-init-checkout on|off] [--track-add-to-cart on|off] [--track-trial-started on|off]
    Add a pixel entry; with a token the Conversions API (server-side events) is on: the token is validated against Meta first. The connect-with-Facebook OAuth path stays in the dashboard
  • integrations meta-capi update <integrationId> [same flags]
    Edit: omitted flags keep their value, an omitted token stays stored
  • integrations meta-capi remove <integrationId> --confirm
    Irreversible
  • integrations manual-payment set --name <n> (--instructions <text> | --instructions-file <path>)
    Configure the manual / wire-transfer payment method (buyers pay by transfer, you approve the order with orders approve)
  • integrations manual-payment remove --confirm
    Remove the manual payment method (idempotent)
  • integrations invoices connect <fakturownia|ifirma|infakt|wfirma|ing|stripe> [--id <id>] --name <n> [--url <url>] (--api-key <k> | --api-key-stdin | --api-key-env VAR) [--account-api-key] [--access-key --secret-key --company-id] [--department-id] [--bank-account]
    Connect (or edit with --id) an invoicing account: credentials verified with the provider, never printed
  • integrations invoices disconnect <client> <accountId> --confirm
    Irreversible
  • integrations payments connect <payu|tpay|p24|paynow|paypal|dodo> --name <n> [--pos-id] [--client-id] [--client-secret] [--second-key] [--safety-key] [--api-key] [--signature-key] [--merchant-id] [--crc-key] [--secrets-env]
    Connect a credential gateway: verified with the gateway first; --secrets-env makes every secret flag take an env-var NAME. Stripe connects in the dashboard
  • integrations payments disconnect <gateway> --confirm
    Irreversible: prices that only offered this gateway become unbuyable
  • integrations payments tpay-blik <on|off>
    Tpay recurring BLIK (BLIK subscriptions)
  • integrations sms twilio connect --account-sid <sid> (--auth-token <t> | --auth-token-stdin | --auth-token-env VAR) [--auth-kind authToken|apiKey --api-key-sid <sid>] [--region us1|ie1|au1] --sender-kind phoneNumber|messagingService --sender <value>
    Connect (or replace) Twilio: verified with Twilio first
  • integrations sms twilio disconnect --confirm
    Secret deleted
  • integrations sms smsapi connect [--region pl|com] (--api-token <t> | --api-token-stdin | --api-token-env VAR) --sender <name>
    Connect (or replace) SMSAPI: verified with SMSAPI first
  • integrations sms smsapi disconnect --confirm · integrations sms smsapi refresh-senders · integrations sms smsapi default-sender <name>
    Token deleted · re-fetch the approved sender names · pick the sending sender
  • integrations sms default-provider <twilio|smsapi>
    Which connected provider campaigns and automations send through

Site

Your public site: platform settings (title, language, theme, branding, main page, privacy, client portal, customer access policy, customer-link base URL), the live flag, slug and custom domains, the storefront layout and the legal documents. Every site update <section> writes only that section atomically; each accepts --json <file|inline> and the typed flags override keys from the JSON. Slug, domains, activation and legal-document writes require --confirm.

  • site get
    Summary table: slug, domains, active, language, main page, theme, branding flags, cookie consent, access policy, communication base URL: zanfia --json site get for the full payload
  • site update general [--title <t>] [--description <t>] [--language pl|en|default] [--show-page-title on|off] [--json <patch>]
    Page title/description, UI language; --json also takes mainPage, mainPageId, mainPageCommunityId
  • site update theme [--theme <color>] [--gray-scale <scale>] [--radius <r>] [--style-variant solid|classic] [--from-brand on|off] [--json <patch>]
    Accent color, neutral scale, radius, style; --json for customColors, customCheckoutTheme, typography
  • site update branding [--logo-url <url|none>] [--logo-dark-url <url|none>] [--favicon-url <url|none>] [--hide-branding on|off] [--json <patch>]
    Logos and favicon (URLs, or Media Library refs logo / logoDark / platformFavicon via --json); hiding the "Powered by Zanfia" badge needs the remove_branding feature (403 otherwise)
  • site update main-page --mode sales|dashboard|community [--page-id <id|none>] [--json <patch>]
    What the site root renders: in sales mode --page-id puts a custom Published page at the root (404 unknown page, 409 not published)
  • site update privacy --cookie-consent on|off [--json <patch>]
    Cookie-consent banner
  • site update portal [--library-width full|boxed] [--library-layout grid|gallery|list] [--mobile-library-layout <l>] [--library-order newest|recently_purchased|alphabetical|custom] [--custom-order <ids>] [--group-by-type on|off] [--default-view library|community] [--messages on|off] [--client-messages on|off] [--clear] [--json <patch>]
    Client-area customization: the whole object is replaced (read site get --json first); --custom-order takes comma-separated product ids top first and implies the custom order; --group-by-type on shows the "All" view as communities first + a section per type instead of one flat list; --clear resets to defaults
  • site update communication --base-url app|platformSlug|platformDomain [--domain <host>] [--json <patch>]
    Which host the links sent to customers use
  • site update access-policy --device-limit <n> [--replacement adaptive|always_allow|creator_approval_only] | --clear [--json <patch>]
    Anti-sharing device limit per customer and what happens when it is hit; --clear (or limit 0) switches it off
  • site activate --on|--off --confirm
    Publish or unpublish the site (the platform "live" flag)
  • site slug set <slug> --confirm
    Change the workspace slug: heavy (re-registers Stripe/Firebase/Vercel hosts, purges caches), every existing link changes; 409 slug-taken
  • site domains set <d1,d2,…>|none --confirm
    Replace the custom-domain list (bare hostnames); 409 domain-taken, 400 restricted-domain; then point DNS at Zanfia
  • site domains verify [<d1,d2>]
    Live DNS + TLS check: status per domain, the DNS records still to add, conflicting records, certificate state
  • site storefront get
    Storefront layout: preset, mode, sections table: --json for the full config
  • site storefront set --json <file|inline>
    Replace the whole storefront config (sections required): edit the output of site storefront get --json and send it back
  • site legal get [--language pl]
    Terms and privacy policy: URL, /documents/<slug> slug and written content per language
  • site legal set [--language pl] [--terms-content <md|file> | --terms-url <url>] [--terms-slug <s>] [--privacy-content <md|file> | --privacy-url <url>] [--privacy-slug <s>] --confirm
    Write a document inline (markdown or a file path → served under /documents/<slug>) or link it out (--*-url); the mode is derived from which flag you pass
  • site library-sections list
    Sections of the member library ("Step 1", "Bonuses": a heading and a filter pill each) in display order with their products, plus the products in no section (they show under Other). Products only, never bundles
  • site library-sections create --name <name> [--products <a,b,...>] [--position <n>] --confirm
    Add a section (last by default; --position is 0-based, the name is unique in the workspace); the listed products move into it from any other section
  • site library-sections rename <sectionId> --name <name> --confirm
    Rename a section (its id and products stay)
  • site library-sections reorder <sectionIds...> --confirm
    Set the section order: pass every section id once, top first
  • site library-sections move <productIds...> --to <sectionId|none> --confirm
    Move products into a section (they leave any other), or to none (Other)
  • site library-sections replace --sections <json|file> --confirm
    Full replace of the ordered list (a JSON array of {id?, name, productIds}): sections left out are deleted and their products go to Other; keep a section by sending its id
  • site library-sections delete <sectionId> --confirm
    Delete a section; its products go back to Other (no product is deleted)

System emails

The emails Zanfia sends to your customers in your name (Site → Emails): per-email subject/headline/layout overrides, the branding frame, and test sends. <key> comes from emails list.

  • emails list
    The customizable system emails with status and effective subject
  • emails get <key>
    One email: subject/headline (EN + PL), layout blocks, variables, link targets
  • emails set <key> [--subject-en <t|none>] [--subject-pl <t|none>] [--headline-en <t|none>] [--headline-pl <t|none>] [--enable | --disable] [--layout=<json|file>] [--reset]
    Customize one email: --layout replaces the whole block layout, --disable only works for optional emails, --reset restores the stock email
  • emails branding get
    The email branding and the site defaults it inherits from
  • emails branding set [--logo-url <url|none>] [--accent-color <hex|none>] [--sender-name <n|none>] [--reply-to <e|none>]
    Header logo, accent colour, From name, Reply-To: none falls back to the site default
  • emails test <key> [--lang en|pl]
    Queue a test send (saved customization, sample data) to your own address

E-mail template library (emails library)

The reusable templates (Marketing → E-mail templates) that workflow "Send email" steps reference: a different thing from the system emails above. Attachments are picked in the dashboard; from the CLI you can only clear them.

  • emails library list
    The library templates with subject, frame and attachment count
  • emails library get <id>
    One template: subject, preheader, sender overrides, attachments, Markdown content
  • emails library create --name <n> [--subject] [--preheader] [--content <md> | --content-file <path>] [--branded | --plain] [--from-name <n>] [--reply-to <e>]
    Create a template: --name is the internal label, not the subject
  • emails library update <id> [same flags] [--clear-preheader] [--clear-from-name] [--clear-reply-to] [--clear-attachments]
    Partial update: workflows that reference the template send the new version from now on
  • emails library duplicate <id>
    Make an independent copy
  • emails library delete <id> --confirm
    Permanent: a workflow step that still references it stops sending until it is re-pointed

Sending identity (emails sender)

  • emails sender get
    Verified sending domains, the default From address and the footer postal address
  • emails sender set-address (--address <line> | --clear)
    The footer postal address rendered under the unsubscribe link of every campaign (one line, max 300 chars): required before a campaign can be sent
  • emails sender warmups
    Each sending domain's warm-up: new domains send campaigns in hourly batches for a few days; shows the state, why a domain counts as warmed and the cap per hour
  • emails sender declare-warmed <domain> --confirm
    "I already send from this domain with another tool": switch the warm-up of a verified domain off at once, recorded under your name (campaigns then go out from it without batches); needs the campaigns.manage permission

Adding, verifying and deleting a sending domain stays in the dashboard (DNS). get and set-address are launch-gated together with campaigns (403 feature_not_available).

One-time offers (OTO)

Reusable post-purchase upsells shown once, full-screen, between payment success and the thank-you step. These commands manage the offer documents only; attach one to a checkout's chain with offers update <cid> --json='{"oto":{"enabled":true,"steps":[{"otoOfferId":"…"}]}}'.

  • oto list
    The workspace's reusable OTO offers
  • oto get <id>
    One OTO offer in full
  • oto create --name <n> --product <pid> --price <priceId> --headline <t> [--product-name] [--description <md>] [--accept-cta <t>] [--decline-text <t>] [--timer-minutes <n|none>] [--json <body>]
    New OTO referencing an existing product and price (creators usually add a dedicated discounted price first); media gallery only via --json
  • oto update <id> [same flags] [--json <body>]
    Edit content / pricing fields; --timer-minutes none clears the countdown
  • oto delete <id> --confirm
    Permanent; checkout oto.steps still referencing the id are left dangling

Order bumps

  • bumps list
    Order bumps in the workspace
  • bumps create --product <pid> --price <priceId> --name <n> [--description] [--cta] [--image <urlOrMediaId>] [--json <fileOrJson>]
    New bump from an existing price (the price must be on the product's default offer). --image sets the bump box image (https URL or a media library id); --json seeds the deep appearance object
  • bumps update <bumpId> [--name] [--description] [--cta] [--product <pid> --price <priceId>] [--image <urlOrMediaId|none>] [--json <fileOrJson>]
    Edit, or point the bump at another product/price. --image sets (or none clears) the bump box image; --json is a deep patch merged into the request body: appearance (media, backgroundColor, borderStyle, borderColor, borderWidth, ctaPosition, ctaSize, ctaBackgroundColor, ctaTextColor, ctaArrowPosition, ctaArrowColor, ctaArrowAnimation, imagePosition, imagePositionMobile, imageWidth); explicit flags win over --json keys
  • bumps delete <bumpId>
    Delete (offers that reference it stop showing it)
  • bumps attach <cid> <bumpId> [--prices id,id]
    Show a bump on an offer (omit --prices = all prices; re-scopes if already attached)
  • bumps detach <cid> <bumpId>
    Remove a bump from an offer

Courses

Address everything by product id (from products list). Statuses: draft, public, hidden, locked; new modules and lessons start as draft. Lesson content is Markdown, and an update replaces the whole body; prefer --content-file for anything long.

  • courses get <pid>
    Full module/lesson structure with statuses
  • courses analytics <pid> [--learners-limit <n>] [--learners-sort progress-desc|progress-asc|last-activity-desc]
    Per-lesson engagement, quiz pass rates and the learner roster
  • courses transcripts <pid>
    Lesson → video/transcript map (read one with media transcript <mediaId>)
  • courses module add <pid> --name <n> [--description]
    New draft module
  • courses module update <pid> <mid> [--name] [--description] [--status] [--unlock-days <n> | --unlock-date <iso> | --clear-unlock] [--require-quiz-pass | --require-all-lessons | --clear-requirement]
    Edit a module: name, status, drip unlock, completion gate
  • courses module delete <pid> <mid>
    Permanent, cascades to all its lessons. To hide instead, set --status hidden
  • courses lesson get <pid> <lid> [--content]
    Lesson detail; --content prints just the Markdown
  • courses lesson add <pid> --module <mid> --title <t> [--type article|video|externalVideo|task|quiz] [--description] [--video-media <mediaId>]
    New draft lesson
  • courses lesson update <pid> <lid> [--title] [--description] [--status] [--content | --content-file]
    Edit a lesson
  • courses lesson set-video <pid> <lid> --video-media <mediaId>
    Bind an uploaded video
  • courses lesson set-thumbnail <pid> <lid> --media <mediaId> | --url <httpsUrl> | --clear
    Lesson thumbnail; video lessons also get it as the player thumbnail
  • courses lesson set-attachments <pid> <lid> --media <id,id,…> | --clear
    Replace the lesson's downloadable attachments with media-library files, in the given order (full list; lesson get shows current uids); --clear removes all
  • courses lesson video get <pid> <lid>
    Chapters, moments and meta tags stored on the lesson video
  • courses lesson video set <pid> <lid> [--chapters=<json|file>] [--moments=<json|file>] [--meta-tags=<json|file>] --confirm
    Replace the lists you pass (each replaces its whole list; absent lists stay untouched). Chapters [{title, start, end}], moments [{label, timestamp}] in seconds, meta tags [{property, value}]
  • courses lesson chapters generate <pid> <lid> [--locale pl|en] [--out <file>]
    AI chapter candidates from the transcript: printed (and optionally written to a JSON file), not saved; apply with courses lesson video set --chapters=<file>. Uses AI credits (402 = credits exhausted)
  • courses lesson moments generate <pid> <lid> [--locale pl|en] [--out <file>]
    AI moment candidates, same flow: apply with courses lesson video set --moments=<file>
  • courses lesson delete <pid> <lid>
    Permanent. To hide instead, set --status hidden
  • courses quiz list <pid> <lid>
    Questions and settings of a quiz lesson
  • courses quiz add <pid> <lid> --type single|multi|truefalse --question <t> [--option <t> …] [--correct 1,3] [--correct-answer true|false] [--explanation] [--points <n>] | --json <file|inline>
    Add a question (choice types: repeated --option + 1-based --correct; true/false: --correct-answer)
  • courses quiz update <pid> <lid> <qid> [same flags | --json <file|inline>]
    Replace a question in full
  • courses quiz delete <pid> <lid> <qid>
    Permanent
  • courses quiz reorder <pid> <lid> --order id,id,…
    Pass every question id
  • courses quiz settings <pid> <lid> [--passing-score <pct> | --clear-passing-score] [--max-attempts <n> | --clear-max-attempts] [--time-limit <s> | --clear-time-limit] [--shuffle-questions on|off] [--shuffle-options on|off] [--show-correct-answers on|off] [--require-pass on|off]
    Grading, attempts, shuffling, gating
  • courses publish <pid> (--module <mid> | --lessons id,id) [--status draft|public|hidden|locked]
    Publish a module and all its lessons, or bulk-change the status of selected lessons
  • courses reorder modules <pid> --order id1,id2,…
    Strict permutation: every module id
  • courses reorder lessons <pid> --module <mid> --order id1,id2,…
    Strict permutation: every lesson id in the module
  • courses questions list <pid> [--filter unanswered|all|public|private|hidden] [--lesson <lessonId>] [--cursor <cursor>]
    Learners' questions under the lessons, newest first, each with its replies (private ones too); default filter all
  • courses questions reply <pid> <questionId> (--content <text> | --content-file <path>) --confirm
    Reply as the course team: the learner sees the reply under the lesson and is notified
  • courses questions status <pid> <entryId> --status published|hidden --confirm
    Hide a question or a reply from learners (the team still sees it), or publish it again
  • courses questions accept <pid> <questionId> (--reply <replyId> | --clear) --confirm
    Mark a reply as the answer to its question; --clear removes the mark

Certificates

Course-completion certificates: issue them for course clients, withdraw or reinstate a credential, delete a revoked one for good, resend the email, and manage designer templates. Certificate ids come from certificates list; template ids from certificates templates list.

CommandPurpose
certificates list [--product <value>] [--limit <n>] [--cursor <value>]List certificates
certificates issue <productId> --client <a,b,...> [--only-completed|--no-only-completed] [--template <value>] [--reissue-revoked|--no-reissue-revoked] --confirmIssue certificates (async)
certificates revoke <certificateId> --confirmRevoke a certificate
certificates reinstate <certificateId>Reinstate a certificate
certificates delete <certificateId> --confirmDelete a revoked certificate
certificates resend <certificateId> --confirmResend the certificate email
certificates templates listList certificate templates
certificates templates create --name <value> [--based-on <value>] [--design-config <json|file>]Create a certificate template
certificates templates get <id>Certificate template detail
certificates templates update <id> [--name <value>] [--template-json <json|file>] [--design-config <json|file>] [--default|--no-default]Update a certificate template
certificates templates delete <id> --confirmDelete a certificate template
certificates settings get <productId>A product's certificate settings
certificates settings update <productId> [--auto-issue|--no-auto-issue] [--template <value>] [--custom-text <value>] [--validity-months <n>] [--code <value>] [--date-mode <completion|training_date|issue_date>] [--training-date <value>] [--claim|--no-claim] [--claim-from <value>]Update a product's certificate settings

Consultations

Consultations booked in the Zanfia calendar. Without --product <productId> (from products list) both commands cover every consultation; pages hold 50 rows by default, at most 100.

  • consultations bookings [--product <productId>] [--from <date>] [--to <date>] [--status booked|cancelled|done] [--limit <n>] [--cursor <cursor>]
    Booked sessions starting in a date range, earliest first, with the client's name and e-mail. --from takes ISO or YYYY-MM-DD (default today 00:00 UTC); --to is exclusive (default --from + 30 days, at most 366 days)
  • consultations without-booking [--product <productId>] [--limit <n>] [--cursor <cursor>]
    Buyers with active access to a consultation who never booked a session (a cancelled booking does not count); external-link consultations are skipped

Newsletters

Address a newsletter product by its product id. An issue has two visibilities: --status public|hidden (the private members-area archive) and --landing public|hidden|locked (the public landing page). Edits are staged on a draft and only reach readers on draft publish.

  • newsletters issues list --product <pid> [--limit] [--cursor]
    Archived issues, hidden included
  • newsletters issues create <pid> --subject <s>
    Manual issue ("write a post without sending"): hidden, article-first, empty content
  • newsletters issues update <iid> --product <pid> [--status public|hidden] [--landing public|hidden|locked] [--render-mode snapshot|article] [--thumbnail <url>|none]
    Visibility, which representation readers see (snapshot = the sent email, article = the markdown), thumbnail
  • newsletters issues copy <pid> --issue <id,id> --to <pid,pid> --confirm
    Copy issues into other newsletter products; non-newsletter targets are reported as skipped
  • newsletters issues draft save <pid> <iid> [--subject <s>] [--content <markdown|file>] [--clear-subject] [--clear-content]
    Stage subject / markdown content on the draft; --content takes inline markdown or a file path
  • newsletters issues draft publish <pid> <iid> [--public] --confirm
    Promote the draft into the published issue (edited content switches render mode to article; the slug is minted on first publish); --public also makes the issue public
  • newsletters issues draft discard <pid> <iid> --confirm
    Drop the staged edits; the published issue stays as is
  • newsletters issues delete <iid> --product <pid> --confirm
    Permanent. To hide instead, set --status hidden
  • newsletters settings update --product <pid> [--archive-enabled on|off] [--archive-access disabled|unlimited|from_subscribe_date] [--publication-delay <days|none>] [--default-status] [--default-visibility] [--archive-inbox=<json|file>] [--double-opt-in none|external] [--client-tag <tag>…]
    Archive, archive inbox, double opt-in and subscriber tags

Communities

Address by community id (from communities list). You act as yourself: channel access and moderation rights are resolved per request. Posts and comments live in feed channels; content is Markdown. Writing requires a member profile in the community, even for workspace staff. Staff-only commands are marked.

  • communities list
    Your communities (with the backing product id)
  • communities channels <cid>
    Channels you can see, with your access level
  • communities posts <cid> <chid> [--limit] [--cursor]
    Posts of a feed channel, newest first
  • communities search <cid> <query> [--limit]
    Full-text search over the published posts you can read
  • communities chat messages <cid> <chid> [--thread <messageId>] [--limit] [--cursor]
    Chat messages, newest first (deleted show as tombstones); --thread lists a thread's replies
  • communities chat send <cid> <chid> (--content <t> | --content-file <path>) [--reply-to <messageId>]
    Send a chat message or a thread reply
  • communities chat edit <cid> <chid> <messageId> (--content | --content-file) [--thread <parentId>]
    Edit an own message
  • communities chat delete <cid> <chid> <messageId> [--thread <parentId>]
    Soft-delete an own message (tombstone; threads survive)
  • communities channel create <cid> --name <n> --type feed|chat|link|support [--description] [--group <gid>] [--url <url>] [--readonly]
    New channel (--url required for link channels; --readonly = announcement mode)
  • communities channel update <cid> <chid> [--name] [--description] [--url] [--readonly true|false]
    Edit a channel
  • communities channel delete <cid> <chid>
    Delete a channel and its content. Cannot be undone
  • communities group create <cid> --name <n> [--description]
    New sidebar section
  • communities group update <cid> <gid> [--name] [--description]
    Edit a section
  • communities group delete <cid> <gid>
    Delete a section. Cannot be undone
  • communities move <cid> (--channel <chid> | --group <gid>) (--into-group <gid> | --to-root | --before <id> | --after <id>)
    Reorder / re-parent the sidebar
  • communities post get <cid> <postId> [--content]
    Post detail; --content prints just the Markdown
  • communities post create <cid> <chid> --title <t> (--content <md> | --content-file <path>)
    New post in a feed channel
  • communities post update <cid> <postId> [--title] [--content | --content-file]
    Edit a post
  • communities post delete <cid> <postId>
    Soft delete (author or moderator)
  • communities post pin <cid> <postId>
    Pin to the top of its channel (staff)
  • communities post unpin <cid> <postId>
    Unpin (staff)
  • communities comments <cid> <postId> [--limit] [--cursor]
    Comments, oldest first; replies carry parentCommentId
  • communities comment create <cid> <postId> (--content <md> | --content-file) [--reply-to <commentId>]
    Comment or threaded reply
  • communities comment update <cid> <commentId> (--content | --content-file)
    Edit a comment
  • communities comment delete <cid> <commentId>
    Soft delete (author or moderator)
  • communities support queue <cid> [--status open|in_progress|pending_customer|resolved|closed] [--limit]
    The support-ticket queue (moderators)
  • communities support mine <cid> [--status]
    Your own tickets
  • communities support show <cid> <ticketId>
    One ticket with its message thread
  • communities support create <cid> --channel <chid> --subject <s> (--message <md> | --message-file <path>)
    Raise a ticket in a support channel
  • communities support reply <cid> <ticketId> (--content <md> | --content-file)
    Reply on a ticket
  • communities support status <cid> <ticketId> <status>
    Change a ticket's status
  • communities members <cid> [--role <roleId> | --disabled true|false | --email-prefix <p>] [--limit] [--cursor]
    Members list (staff); the three filters are mutually exclusive
  • communities member set-roles <cid> <memberId> <roleId...>
    Fully replace a member's role set (staff)
  • communities member disable <cid> <memberId>
    Reversible soft-ban (staff)
  • communities member enable <cid> <memberId>
    Re-enable a disabled member (staff)
  • communities member mute <cid> <memberId> --minutes <n>
    Timeout for 1..40320 minutes (staff)
  • communities member unmute <cid> <memberId>
    Lift an active mute (staff)
  • communities settings get <cid>
    The settings document (null = defaults apply) (staff)
  • communities settings update <cid> [--patch=<json|file>] [--events-enabled on|off] [--direct-messages on|off] [--content-visible-from-join-date on|off] [--gamification on|off] [--auto-hide-after-reports <n|off>]
    Patch settings: JSON patch keys: uploadVideo, membersDisplay, viewStyle, displayName, icon, publicAccess, gamification, moderation, assistant, digest, …; nested blocks in the patch replace their sub-object, the toggle flags merge (staff)
  • communities roles <cid>
    Roles in position order (Owner first)
  • communities role create <cid> --name <n> [--color <hex>] [--icon] [--position <n>] [--permissions=<json|file>] [--content-access=<json|file>]
    New custom role (staff)
  • communities role update <cid> <roleId> [--name] [--color] [--icon] [--permissions=<json|file>] [--content-access=<json|file>]
    Edit a role: the built-in Owner role is immutable (staff)
  • communities role delete <cid> <roleId> --confirm
    Delete a role: holders fall back to Member (staff)
  • communities role reorder <cid> <roleId...>
    Pass every role id in the new order, Owner first (staff)
  • communities events list <cid> [--from <iso>] [--to <iso>] [--exclude-cancelled] [--limit]
    Calendar events, soonest first
  • communities events create <cid> --title <t> --start <iso> --end <iso> --timezone <iana> --location=<json|file> [--type liveCall|workshop|meetup|conference|retreat|other] [--all-day] [--description] [--json <file>]
    New event (staff); --location = {"type":"online","url":…} or {"type":"inPerson","address":…}
  • communities events update <cid> <eventId> [same flags] [--all-day on|off] [--status scheduled|cancelled] [--json <file>]
    Edit / cancel / restore an event (staff)
  • communities events delete <cid> <eventId> --confirm
    Delete an event (staff)
  • communities join-requests list <cid>
    People waiting to join a community that approves members by hand: pending requests first (with their answers and the AI hint), then declined ones. Workspace staff with member management; member keys get 403
  • communities join-requests approve <cid> <requestIds...> --confirm
    Approve requests: the normal join runs (welcome e-mail with the login link, welcome DM, notifications); up to 20 ids per call, repeats change nothing
  • communities join-requests decline <cid> <requestIds...> --confirm
    Decline requests quietly (a neutral e-mail, no reason given); up to 100 ids per call, repeats change nothing
  • communities digest list <cid>
    The weekly digest e-mail to members: this week's draft with the preview of what members get, then the earlier weeks
  • communities digest approve <cid> [--week <weekKey>] --confirm
    Approve the week (nothing goes out until the owner or an admin approves): the digest is sent Thursday 09:00, at once when approved later that Thursday; repeats never send twice
  • communities digest skip <cid> [--week <weekKey>] --confirm
    Skip the week: no digest goes out (refused once the send has started); owner / admin only
  • communities moderation queue <cid>
    Reported posts / comments: pending plus recently resolved (staff)
  • communities moderation resolve <cid> <reportId> --action remove|dismiss [--note] --confirm
    Remove the content (author notified) or dismiss the report

Conversations (client DMs)

The creator ↔ client direct-message inbox. Conversations are keyed by client id (wcid).

  • conversations list [--unread] [--limit] [--cursor]
    Inbox, newest activity first
  • conversations get <clientId>
    One conversation (inbox row)
  • conversations messages <clientId> [--limit] [--cursor]
    Messages, newest first (deleted show as tombstones)
  • conversations send <clientId> (--content <t> | --content-file <path>) [--message-id <id>]
    Send a staff message; --message-id is an idempotency key
  • conversations edit <clientId> <messageId> (--content | --content-file)
    Edit an own message
  • conversations delete <clientId> <messageId> --confirm
    Soft-delete an own message (tombstone for both sides)
  • conversations react <clientId> <messageId> --emoji <emoji>
    Toggle your emoji reaction
  • conversations read <clientId>
    Mark as read (shared team read marker)
  • conversations broadcast (--recipients <id…> | --recipients-file <path>) (--content | --content-file) [--notify-by-email] --confirm
    DMs every listed client (max 5000), irreversibly; the fan-out runs async

Clients

  • clients list [--search <q>] [--with-products | --without-products] [--created-from <date>] [--created-to <date>] [--limit] [--cursor]
    Browse clients, newest first. --created-from / --created-to filter by the date the client was added (YYYY-MM-DD or an ISO 8601 date-time; a plain --created-to day keeps the whole day)
  • clients add --email <e> [--first-name] [--last-name] [--phone] [--language]
    Create a contact only: product access is granted separately
  • clients get <id>
    One client
  • clients tags / clients tags list
    The workspace tag registry: the ids clients import --tag, segments and campaign link actions refer to
  • clients tags create --name <n> [--color <c>]
    Create a tag (names are unique per workspace; a duplicate answers 409)
  • clients tags update <tagId> [--name <n>] [--color <c> | --clear-color]
    Rename / recolour: the id stays, so every assignment, segment and link action keeps pointing at it
  • clients tags delete <tagId> --confirm
    Permanent: the tag is removed from every client that carries it
  • clients tags assign --id <a,b,…> [--add <tagId,…>] [--remove <tagId,…>]
    Add / remove tags on many clients at once (tag ids; batches of 200)
  • clients email-log <id> [--limit <n>]
    Emails Zanfia sent to the client and their delivery status
  • clients bundled-access <productId>
    Clients whose access to the product comes from a PARENT product's plan (bundle) rather than a direct grant; with clients list --product this is the product's full access census
  • clients accesses <id> [--status active|revoked|expired]
    Every product-access grant the client holds, newest first
  • clients notes <id>
    Internal CRM notes (team-only, never shown to the customer)
  • clients add-note <id> --text <t>
    Add an internal note (up to 5000 characters)
  • clients update <id> [--first-name] [--last-name] [--phone] [--language]
    Edit the profile
  • clients grant <productId> --email <e> [--first-name] [--last-name] [--price <priceId>] [--plan <planId>] [--until <date>] [--welcome-email]
    Grant product access without a purchase (free enrollment under an existing price / plan, optionally time-limited; silent unless --welcome-email)
  • clients revoke <productId> --email <e> [--run-automations] [--cancel-subscription] [--silent]
    Revoke access: never refunds; a subscription Stripe already ended is revoked, a live one is refused unless --cancel-subscription cancels it in the same step; --silent skips e-mails and automations
  • clients change-period <productId> --email <e> (--end <date> | --add-days <n>)
    Adjust a time-limited access window (absolute end or relative days; negative shortens)
  • clients cancel-subscription <productId> --email <e> --confirm
    Stop a client's subscription billing: access runs to the end of the paid period; a subscription Stripe already ended is reported as already ended and the access is brought in line with Stripe (409 = not a subscription / gateway not connected)
  • clients change-email <id> --email <e>
    Change the email (profile, product access and login move with it)
  • clients import <csvFile> [--tag <tagId> …]
    Bulk-import contact-only clients from a CSV (email + optional firstName/lastName/phoneNumber/language); batches of 100, existing emails deduped
  • clients merge-preview --survivor <wcid> --duplicate <wcid>
    Dry-run a merge: shows what would happen and prints the previewHash the merge requires
  • clients merge --survivor <wcid> --duplicate <wcid> --preview-hash <hash>
    Irreversibly merge the duplicate into the survivor (grants move/dedupe, community content re-points, duplicate tombstoned); a stale hash is refused (409); live subscriptions block it
  • clients bulk-revoke <productId> (--email a,b,… | --all) [--run-tasks] --confirm
    Revoke many clients' access in one background run (no refunds): 202 with the Trigger run id; progress in the dashboard Tasks tab
  • clients bulk-change-period <productId> (--email a,b,… | --all) (--end-date <iso> | --add-days <n>) --confirm
    Shift many clients' access end at once (absolute date or relative days): 202 with the run id
  • clients bulk-delete <productId> (--email a,b,… | --all) --confirm
    Remove many clients' grants on a product in one run (records stay; use clients delete for those): 202 with the run id
  • clients bulk-revoke-filtered --product <id,id> --filter=<json|file> [--run-tasks] --confirm
    Revoke every client matching a Customers-list filter (productsFilter, search, tagIds, productIds, emailSubscriptionStatus, minTotalSpent, ordersFilter, …) in up to 10 products: resolved server-side, 202 with the run id
  • clients delete --id <a,b,…> --confirm
    Delete (tombstone) clients by id: ids with active product access are skipped (revoke first); batched by 200
  • clients export [--out <file>] [--columns tags,stats,emailSubscription,activity,accesses,invoiceData,attribution,identifiers] [--created-from <iso>] [--created-to <iso>] [--limit <n>] [--search] [--with-products|--without-products]
    Clients CSV written to --out (default zanfia-clients.csv): identity columns always, extra groups via --columns; reports the row count and whether the cap truncated it
  • clients devices list <id>
    The client's active device slots under the customer access policy (site update access-policy): id, kind, name, first/last seen, active until
  • clients devices revoke <id> <deviceId> --confirm
    Sign one device out; it must claim a slot again
  • clients devices reset <id> --confirm
    Sign every device out and clear the client's slots: reports revokedCount
  • clients email-subscription --id <a,b,…> --status subscribed|unsubscribed [--reason imported-from-other-list|existing-customers-consent|asked-directly|other] [--reason-note <t>] --confirm
    Subscribe / unsubscribe many clients to marketing e-mail. Subscribing records the consent basis you attest to (--reason is required then); batches of 200
  • clients sms-subscription --id <a,b,…> --status subscribed|unsubscribed [--reason explicit-sms-consent|asked-directly|other] [--reason-note <t>] --confirm
    The same for marketing SMS (a separate consent: an e-mail opt-in never covers SMS)
  • clients emails inbox [--limit <n>]
    One-off client e-mails across every customer: drafts and sent
  • clients emails list <clientId>
    One customer's e-mails, newest first
  • clients emails draft <clientId> --subject <s> (--content <md> | --content-file <path>) [--from-name] [--reply-to] [--from-email] [--branded | --plain]
    Create a draft personal e-mail to one customer. Nothing is sent
  • clients emails update <clientId> <emailId> [same flags]
    Edit a draft (a sent e-mail is a record)
  • clients emails delete <clientId> <emailId> --confirm
    Delete a draft
  • clients emails send <clientId> <emailId> --confirm
    ⚠️ Real send: the e-mail goes to the customer and cannot be recalled. A failed hand-off leaves the draft re-sendable
  • clients emails delete-drafts (--ids <a,b,...> | --subject <subject> | --all) [--preview] [--confirm]
    Delete drafts (and failed hand-offs) across customers: the listed ids (from clients emails inbox), every draft with exactly this subject, or all; sent e-mails are never touched. --preview only counts; a delete needs --confirm and never removes more than it counted
  • clients automation-runs list <clientId>
    One customer's workflow runs in progress (as on the customer card), across every workflow, newest first; test runs excluded
  • clients automation-runs stop <clientId> --confirm
    Take the customer out of every workflow run in progress: each run ends cancelled and no further step runs (a step already sending finishes); they can enter again when a trigger fires later. One run only: workflows runs stop

A 503 from the bulk commands means the Trigger.dev scheduler is not configured on that environment.

Campaigns (email broadcasts)

Drafts are harmless; campaigns send emails real people. Audience is either a saved segment (--segment, re-resolved at send time) or every subscribed client (--all-subscribed, the default).

  • campaigns list
    Email campaigns
  • campaigns get <id>
    One campaign: status, sender, audience, stats (delivery funnel + engagement: unique opens/clicks with rates over the tracked base, attributed orders), content
  • campaigns create --name <n> [--subject] [--preheader] [--content <md> | --content-file <path>] [--from-name <n>] [--reply-to <e>] [--from-email <e>] [--segment <segmentId> | --all-subscribed] [--background-color <hex>] [--content-background-color <hex>] [--link-color <hex>]
    Create a draft. Nothing is emailed; only --name is required now (subject/content/sender are enforced by send); --from-email needs the workspace's verified sending domain
  • campaigns update <id> [same flags]
    Edit a draft (campaigns past draft are immutable except cancel); --content replaces the whole body
  • campaigns delete <id>
    Permanent: draft/cancelled/failed only (sent campaigns are audit records and stay)
  • campaigns test <id> [--to <email>]
    Send a test of the saved campaign to up to 3 addresses (--to repeatable or comma-separated; default: your own address). Subject prefixed [Test], sample personalization values; never the audience
  • campaigns send <id> [--at <iso>] --confirm
    Sends real bulk email to the whole resolved audience, irreversibly; omit --at to send now, --at schedules (max 30 days out)
  • campaigns cancel <id>
    Cancel a scheduled or in-flight campaign: stops the remaining sends only
  • campaigns reschedule <id> --at <iso>
    Move a scheduled campaign to a new send time (min 2 minutes, max 30 days out); it stays scheduled and goes out only at the new time. Drafts: campaigns send --at
  • campaigns recipients <id> [--status pending|sent|delivered|skipped-suppressed|skipped-muted|failed|bounced|complained|clicked] [--link <linkId>] [--limit] [--cursor]
    Recipient drill-down of the report with per-recipient opens/clicks (- = not measured); --link lists who clicked one link (id from campaigns links)
  • campaigns links <id>
    Link table of a sent campaign: per-link unique/total clicks, most-clicked first

SMS campaigns

One-off text broadcasts sent through the workspace's own Twilio / SMSAPI account (connect it in the dashboard first). Drafts are harmless; sms-campaigns send texts real people, is billed by your provider per message part and cannot be undone. The audience is every SMS-subscribed client (--all-subscribed, the default) or a saved segment; a segment is always narrowed to clients with a phone number and an explicit SMS consent.

  • sms-campaigns list
    Campaigns with status, audience, recipients, sent count and billed parts
  • sms-campaigns get <id>
    One campaign: provider, sender, audience, stats, opt-out line, body
  • sms-campaigns create --name <n> [--body <t> | --body-file <path>] [--opt-out-hint <t>] [--provider twilio|smsapi] [--sender <name>] [--segment <segmentId> | --all-subscribed]
    Create a draft. Nothing is sent. The body supports {{firstName}} / {{lastName}}
  • sms-campaigns update <id> [same flags] [--clear-opt-out-hint] [--default-provider] [--default-sender]
    Drafts only: anything past draft is immutable except cancel
  • sms-campaigns delete <id> --confirm
    Permanent; draft / cancelled / failed only (sent campaigns are audit records)
  • sms-campaigns send <id> [--at <iso>] --confirm
    ⚠️ Real send. Omit --at to send now; --at schedules (max 30 days out)
  • sms-campaigns cancel <id>
    Stop a scheduled or in-flight campaign: only the remaining sends
  • sms-campaigns recipients <id> [--status pending|sent|failed|skipped-invalid-number] [--limit] [--cursor]
    Per-recipient drill-down with billed parts and provider details

A 503 from sms-campaigns send means the Trigger.dev scheduler is not configured on that environment.

Segments

Saved, ANDed condition sets over the clients list: the audiences campaigns target. Condition flags are repeatable; --json-file takes the conditions as a JSON array instead. --owns / --not-owns count effective access (a direct grant or access through a bundle); use --owns-direct <pid> / --not-owns-direct <pid> to match only a direct grant on that product (bought or granted by hand); in JSON, "accessScope": "direct" on the product condition.

  • segments list
    Audience segments
  • segments create --name <n> [--has-tag <tagId>] [--without-tag <tagId>] [--owns <pid>] [--not-owns <pid>] [--spent-gte <minor>] [--spent-lte <minor>] [--subscription pending|subscribed|unsubscribed] | --json-file <path>
    New segment (name unique per workspace, max 80 chars)
  • segments update <id> [--name] [condition flags | --json-file <path>]
    Rename and/or replace the condition set wholesale
  • segments delete <id>
    Delete: sent campaigns are unaffected; drafts targeting it fail to send until re-pointed
  • segments preview [condition flags | --json-file <path>]
    Count the matching clients without saving anything

Signup forms

Standalone signup forms (newsletter / lead capture). forms list also shows legacy product-bound forms, which stay editable only via the product editor.

  • forms list
    Signup forms (standalone + legacy product-bound, flagged)
  • forms get <id>
    One form: settings + full embed config
  • forms create --name <n> [--tags <id,id>] [--opt-in double|single] [--redirect <url>] [--header] [--description] [--button-text] [--success-message] [--phone off|optional|required] [--sms-consent on|off] [--sms-consent-text <t>] [--variant basic|card|richCard|minimal|split|gradient]
    New standalone form (double opt-in by default)
  • forms update <id> [--name] [--tags <ids>] [--opt-in double|single] [--redirect <url|none>] [--confirmation-subject] [--confirmation-intro] [--clear-confirmation-email] [--header] [--description] [--button-text] [--success-message] [--variant] [--json <patch>]
    Edit: flat config flags are merged over the current embed config for you; a --json config object is sent as the full replacement (flat flags still win); none clears
  • forms delete <id> --confirm
    Permanent; legacy product-bound forms are refused by the API
  • forms submissions <id> [--limit <n>]
    Submissions, newest first
  • forms submit <formId> --email <e> [--first-name] [--phone] [--language] [--tags <id,id>] [--field key=value] [--page-url <url>] [--ref-code <c>] [--marketing-consent|--no-marketing-consent] [--consent-statement] [--json <body>] --confirm
    Sign a person up through a form as your own server does: consent as clients add, a double opt-in form e-mails a confirmation, fields overwrite, the form's automations start
  • forms snippet <id> [--lang pl|en] [--first-name] [--redirect <url>] [--platform-url <origin>]
    HTML of the native form for ANY website (form markup + the platform script). See Your form on your own site

--phone adds a phone number field to the form; --sms-consent on adds the marketing-SMS consent checkbox (it needs the phone field; --phone off switches it off too). The full per-form copy of the double opt-in confirmation e-mail is set through forms update --json with a confirmationEmailOverride object (null restores the workspace copy); --confirmation-subject / --confirmation-intro remain as the short legacy form.

Testimonials (wall of love)

Moderation, manual adds, collection forms, and asking customers for a testimonial. Statuses: pending, approved, hidden.

  • testimonials list [--status pending|approved|hidden] [--product <pid>] [--tag <t>] [--limit] [--cursor]
    Management view: pending/hidden included
  • testimonials add --quote <t> --author <n> [--type text|imported] [--rating 1-5] [--role] [--avatar-url] [--products <id,id>] [--tags <a,b>] [--imported-from <source>] [--client <clientId>]
    Add a text testimonial or record an imported one (video adds are not supported)
  • testimonials update <id> [--approve | --hide | --pending] [--feature | --unfeature] [--quote] [--rating <1-5|none>] [--author] [--role <r|none>] [--avatar-url <url|none>] [--products <ids>] [--tags <tags>] [--client <id|none>]
    Moderation sugar plus field patches; none clears nullable fields, empty string clears lists
  • testimonials delete <id> --confirm
    Permanent
  • testimonials request --client <clientId> [--product <pid>] [--form <formId>] --confirm
    Sends a real email asking the customer for a testimonial: the form is resolved server-side (--form, else the product's active form, else the workspace-general one)
  • testimonials forms list
    Collection forms (the public submission pages)
  • testimonials forms create --name <n> [--slug] [--product <pid>] [--headline] [--questions "Q1|Q2"] [--collect-rating on|off] [--collect-role on|off] [--allow-video on|off] [--thank-you <msg>]
    New collection form (omit --product = workspace-general)
  • testimonials forms update <id> [--name] [--slug] [--product <pid|none>] [--headline <t|none>] [--questions "Q1|Q2"] [--collect-rating on|off] [--collect-role on|off] [--allow-video on|off] [--thank-you <msg|none>] [--activate | --deactivate]
    Edit / activate / deactivate a collection form
  • testimonials forms delete <id> --confirm
    Permanent

Brand profiles

Extracted or hand-built brand identity (colors, fonts, voice) used by content generation and site update theme --from-brand.

  • brand list
    Brand profiles
  • brand get <id>
    One profile: also how you poll a pending extraction
  • brand create --name <n>
    Empty profile to fill in with brand update
  • brand extract <url> [--name <n>] [--into <brandProfileId>]
    Extract a profile from a website (async: starts pending; poll brand get until ready/failed); --into re-extracts into an existing profile, preserving manually edited fields
  • brand update <id> [--name <n>] [--json-file <path>]
    Rename, or patch { name?, visual?, editorial? }: the given top-level blocks are replaced wholesale
  • brand delete <id>
    Permanent: no undo, no archive; content generation that referenced it falls back to no brand context

Team

Workspace team members and invitations. Roles: admin, editor, content_creator, viewer.

  • team members
    Team members
  • team invitations
    Pending invitations (accepted/revoked/expired ones are history)
  • team invite <email> --role admin|editor|content_creator|viewer [--name <n>]
    Invite someone: they receive the accept link by email
  • team resend <invitationId>
    Re-email a pending invitation with a fresh link (60 s spacing)
  • team revoke <invitationId>
    Revoke a pending invitation: the link stops working immediately
  • team set-role <userId> <role>
    Change a member's workspace role
  • team remove <userId>
    Remove a member: access lost immediately; cannot be undone (re-invite to restore)

Orders

  • orders list [--product] [--status paid|pending|refunded|failed] [--from/--to] [--search] [--include-test] [--limit/--cursor]
    Order history, newest first: manual wire-transfer orders awaiting your decision are --status pending; simulated test purchases only with --include-test
  • orders get <orderId>
    One order: payment, refund, order bumps, invoice
  • orders approve <orderId> [--issue-invoice] --confirm
    Approve a manual (wire-transfer) order: grants access and sends the purchase email; --issue-invoice also issues an invoice via the connected invoicing account (409 = already decided)
  • orders reject <orderId> --confirm
    Reject a manual order: cancels it and notifies the buyer (409 = already decided)
  • orders mark-test <orderId> [--undo] --confirm
    Mark a real order as a test: it leaves revenue, charts and reports, the buyer keeps access; --undo puts it back (409 = already in that state)
  • orders mark-refunded <orderId> [--revoke-access] [--note <text>] --confirm
    Record that you returned the money outside Zanfia (bank transfer, cash): nothing is charged or paid out and no correcting invoice is issued (do that in your invoicing tool); the order counts as refunded. --revoke-access also ends the buyer's access (refused while a live subscription would keep billing); --note goes on the order's audit trail (max 500 chars)
  • orders export --from <date> --to <date> [--product] [--status] [--search] [--include-test] [--max-rows <n>] [--out <file>]
    Orders CSV to a file: the --from/--to window is required; same filters as orders list (row cap default 10000, max 20000)

Subscriptions

Recurring-billing follow-ups: failed charges, customers on manual-renewal plans, and subscriptions that are ending.

  • subscriptions failed
    Failed subscription / installment charges: customer, product, amount, attempts and Stripe's next automatic retry, invoice id, hosted invoice URL; notes when Stripe is not connected (live dunning fields unavailable)
  • subscriptions retry <invoiceId> --confirm
    Retry a failed charge by its Stripe invoice id (in_…): a real charge attempt; result is paid, requires_action (share the hosted invoice URL), card_declined, not_eligible or failed
  • subscriptions upcoming
    Customers on manual-renewal plans whose access ends soon: days left, last reminder, and the renewal link they need to click
  • subscriptions ending
    Live subscriptions set to cancel at period end: still paying until the date shown, then they churn unless the customer reverses the cancellation

Instalment arrears (subscriptions arrears)

Buyers whose instalment plan stopped being paid before its last instalment (checked live in Stripe; a buyer who paid outside Stripe is not listed), and the workspace setting for such a non-payment: revoke ends the access like a plain subscription end, keep leaves it and lists the buyer. The default is revoke; the setting applies to plans bought since it shipped, older plans always keep their access, and a plan paid in full is never affected. subscriptions arrears keep on one buyer („Zostaw”) only saves a mark (who, when): the access, Stripe and the buyer are not touched.

CommandPurpose
subscriptions arrears settingWhat happens when a buyer stops paying instalments
subscriptions arrears set-setting --action <revoke|keep> --confirmChange what happens when a buyer stops paying instalments
subscriptions arrears list [--product-id <value>] [--include-kept|--no-include-kept]List buyers behind on instalments („Zaległości”)
subscriptions arrears keep <productId> <clientEmail> --confirmKeep a behind-on-instalments buyer's access („Zostaw”)

Stats

Range via --period 24h|7d|30d|90d or --from/--to (ISO date or instant); --currency forces the report currency.

  • stats overview
    High-level revenue / order overview
  • stats sales [--period] [--from] [--to] [--currency]
    Sales report: revenue, refunds, AOV, per-product breakdown
  • stats recurring [--currency]
    MRR / ARR snapshot of the active recurring subscriptions
  • stats mrr-history [--period] [--from] [--to] [--currency]
    MRR over time + churn series from the daily snapshots
  • stats funnel [--period] [--from] [--to]
    Checkout funnel: sessions → conversions, per checkout
  • stats traffic [--period] [--from] [--to]
    Page traffic: views / entries / purchases per page and source
  • stats sessions [--period] [--from] [--to] [--limit]
    Recent checkout sessions across every checkout
  • stats product <id> [--currency]
    Per-product pulse (last 30 days): revenue, students, engagement, attention
  • stats conversion-events [--order] [--checkout] [--session] [--status a,b] [--client a,b] [--limit]
    Conversion-tracking event log: did the Purchase reach Meta / GA4 / TikTok, and with what result
  • stats conversion-health [--since-days <n>]
    Per-integration delivery health verdict (healthy / degraded / failing / inactive)

Discounts

  • discounts list
    Discount codes
  • discounts create --code <CODE> (--percent-off <pct> | --amount-off <minor> --currency <c>) [--product <id> …] [--max-redemptions <n>] [--starts <iso>] [--ends <iso>] [--forever] [--per-unit] [--inactive]
    New code: exactly one of --percent-off / --amount-off; --forever applies to every subscription renewal (default: first payment only); --per-unit takes --amount-off off each unit bought (default: once per order)
  • discounts update <discountId> [--percent-off | --amount-off --currency] [--product <id> …] [--max-redemptions <n> | --clear-max-redemptions] [--ends <iso> | --clear-ends] [--forever] [--amount-scope order|unit] [--active | --inactive]
    Change value, products, limits, dates, active state; --amount-scope switches a fixed amount between once per order and per unit
  • discounts archive <discountId>
    Soft delete: the code stops working at checkout
  • discounts delete <discountId>
    Permanent: prefer discounts archive

Referrals

Referral and affiliate programs: a program targets one or more products, enrolled clients get a personal ref code, and purchases attributed to a code count as referrals. Milestone rewards (referral kind) are approved automatically or by you; rewards whose delivery failed can be retried. Program ids come from referrals list, participant and ledger-entry ids from referrals get. The deep config (milestones, commission, attribution) travels as one JSON object: --json <file|inline>.

  • referrals list
    Programs with kind, status, enrollment mode, product count and headline stats (participants, valid referrals, rewards pending approval)
  • referrals get <programId>
    Program dashboard: summary, participants (ref codes, stats), rewards pending approval and failed deliveries: --json dumps the full documents
  • referrals create --kind referral|affiliate --name <n> --product <id,id,…> [--enrollment open|inviteOnly|closed] [--status active|inactive] [--json <json|file>]
    Create a program: --json carries milestones (reward ladder), commission (affiliate terms) and attribution (window, coupon attribution, two-sided discount); flat flags win over --json keys
  • referrals update <programId> [--name <n>] [--status active|inactive] [--product <id,id,…>] [--enrollment <mode>] [--json <json|file>]
    Update a program: --product and --json milestones are full replaces (read the program first)
  • referrals archive <programId> --confirm
    Archive a program: new referrals stop counting and it leaves the active list
  • referrals enroll <programId> (--client <clientId> | --email <email>) [--first-name <n>] [--last-name <n>]
    Enroll one client as a participant and print their ref code: idempotent, a repeat reports alreadyEnrolled
  • referrals enroll-all <programId> --confirm
    Enroll every client with access to one of the program products: reports enrolled / alreadyEnrolled / skippedWithoutAccount (legacy access without a login account)
  • referrals resolve <programId> <entryId> --decision approve|reject --confirm
    Approve (delivers the reward) or reject a reward waiting for manual approval; the reply says whether delivery already happened
  • referrals retry-delivery <programId> <entryId>
    Re-run delivery for a reward listed under failed deliveries

Workspace

Workspace-level settings. The timezone drives workspace-scoped scheduling such as workflow "wait until" steps. The video quality profile depends on the plan and stays in the dashboard.

  • workspace / workspace get
    Name, timezone, auto-transcription and the default transcription language
  • workspace update [--name <n>] [--timezone <iana> | --inherit-timezone] [--auto-transcribe on|off] [--transcription-language pl|en|default]
    Change settings. --inherit-timezone clears the override so the workspace follows the owner's account timezone

Workflows

The CLI inspects workflow automations, pauses/resumes them, (re)starts runs, and can author them: create a draft, replace the draft graph with the same JSON the dashboard builder saves, publish, duplicate and delete. Publishing makes a workflow live (real emails, SMS and webhooks fire on real customer events), so it requires --confirm. The visual builder in the dashboard stays the comfortable way to design a graph; workflows draft set is for scripts and agents. Runs execute for real (emails, tags and webhooks fire), so the run commands require --confirm.

  • workflows list
    Automations with status, triggers, product scope, node counts, last run
  • workflows get <id> [--graph]
    One workflow in detail. --graph walks the steps in execution order (trigger → actions → delays → condition branches), with each email's subject and a short plain-text excerpt of its body (library emails show their name and id). Shows the published graph and, when it differs, the draft below it; with -o json the raw response carries draftGraph, publishedGraph and hasUnpublishedChanges
  • workflows runs <id> [--limit]
    Recent runs, newest first
  • workflows runs stop <id> <runId> --confirm
    Take the person out of one run: it ends cancelled and nothing after the current step runs (a step already sending finishes); a finished run is left as it is, and they can enter again when the trigger fires later
  • workflows pause <id>
    Stop reacting to trigger events and cancel runs in progress (contacts waiting in a delay get no further steps)
  • workflows resume <id>
    Restart a paused workflow (needs a published version)
  • workflows retry <id> <runId> --confirm
    Re-run a finished run with its original trigger context as a new run (202 → runId)
  • workflows test-run <id> --context=<json|file> [--trigger-node <nodeId>] --confirm
    Start a run from a hand-written trigger context (clientEmail, clientFirstName, productId, orderId, …): not a dry run; a 503 means the Trigger.dev scheduler is not configured on that environment
  • workflows create --name <n> [--description <t>] [--product <id,id>] [--trigger <eventType>] [--kind transactional|marketing]
    Create a draft. Nothing runs. Without --trigger the canvas starts empty. --kind marketing = unsubscribe link in every e-mail and e-mail/SMS steps skipped for unsubscribed contacts; omitted = derived from the triggers at publish
  • workflows draft get <id> [--published] [--out <file>]
    Print the draft graph as JSON (or the live one with --published); edit it and feed it back to workflows draft set
  • workflows draft set <id> --json <file|json> [--name] [--description] [--product <id,id>] [--kind]
    Replace the draft graph ({ nodes, edges }); the published version keeps running untouched. A 422 lists every validation issue with its node id and reason code
  • workflows publish <id> --confirm
    ⚠️ Make the draft live. The graph must validate (422 otherwise)
  • workflows duplicate <id> [--name <n>]
    Copy a workflow: the copy is an unpublished draft
  • workflows delete <id> --confirm
    Permanent; workflows pause is the reversible alternative

Funnels

Marketing funnels are named step chains that reference existing checkouts, pages and workflows: a funnel never owns the artifacts its steps point to. Creation is recipe-first; from-template also generates the page steps with AI.

  • funnels list
    Funnels with status, recipe, step count
  • funnels get <id>
    One funnel's full step chain, workflows and products; a webinar funnel adds its event (start, join link, every reminder with its send moment and state)
  • funnels create --recipe <lead_magnet|tripwire|low_ticket|sell_product|webinar> --product <id> --lang en|pl [--name]
    Draft funnel over the product's existing checkouts plus the recipe email sequence as a draft workflow (409 when the product lacks the free/paid checkout the recipe needs). webinar takes any product and creates three empty page steps; the output names the next command (funnels generate-page <id> --step 1 --event-start …)
  • funnels update <id> [--name] [--workflows a,b] [--json <patch>]
    Rename, swap workflows, or re-wire the step chain (steps/workflowIds are full replaces)
  • funnels status <id> --set draft|live|archived
    Status transitions (all reversible)
  • funnels delete <id> --confirm
    Hard-delete the funnel document only; prefer archiving
  • funnels generate-page <id> --step <n|stepId> --lang en|pl [--brief] [--template] [--event-start <iso>] [--event-timezone] [--event-duration <min>] [--event-mode live|on_demand] [--join-url <url>]
    AI-generate the page for one page-type step and wire it (queued, 1-5 minutes; consumes AI credits). A webinar registration step builds the whole family around the --event-* values; --join-url is the ClickMeeting / Zoom link the pages and reminders carry
  • funnels templates
    The template catalog behind from-template (id, recipe, product requirement, step chain; available: no = coming soon)
  • funnels from-template <templateId> --product <id> --lang en|pl [--name] [--event-start <iso>] [--event-timezone] [--event-duration <min>] [--event-mode live|on_demand] [--join-url <url>] [--live-room <roomId>] [--page-template <id>] --confirm
    Create the funnel from a template and queue the AI generation of its page steps (202 → per-step runId table + skipped steps; consumes AI credits); --page-template seeds every page with one design template (e.g. course-fresh, thank-you steps take its companion). --join-url gives a live webinar its ClickMeeting / Zoom link (watch page button, calendar, thank-you page, reminders). --live-room runs the webinar template on a webinar that plays a recording, without --product (needs the live_rooms plan feature). Poll funnels get <id> until the steps have pages
  • funnels from-pages --page <id,id,…> [--name] [--json <file>]
    A funnel record over existing hosted pages in visitor order (first = entry); nothing is generated
  • funnels stats <funnelId>
    Per-step numbers (views / entries / signups, checkout sessions / completed, OTO viewed / accepted) plus totals; n/a = not applicable to that step kind

Blog

The workspace blog (Your site → Blog): Markdown posts served at /blog/{slug} on your store host, with an RSS feed at /blog/feed. Drafts are visible to your team only; publish makes a post public. Walkthrough and the assistant's part: Blog with the assistant.

CommandPurpose
blog listList blog posts
blog create --title <value> [--slug <value>] [--content <value>] [--excerpt <value>] [--cover <value>] [--tags <a,b,...>] [--category-ids <a,b,...>] [--seo <json|file>] [--publish|--no-publish] [--published-at <value>]Create a blog post
blog get <id>Blog post detail
blog update <id> [--title <value>] [--slug <value>] [--content <value>] [--excerpt <value>] [--cover <value>] [--tags <a,b,...>] [--category-ids <a,b,...>] [--seo <json|file>] [--published-at <value>]Update a blog post
blog publish <id> [--publish|--no-publish]Publish or unpublish a blog post
blog delete <id> --confirmDelete a blog post
blog schedule <id> --at <value>Schedule a blog post
blog categories listList blog categories
blog categories create <name> [--slug <value>] [--description <value>] [--seo <json|file>]Create a blog category
blog categories update <id> [--name <value>] [--slug <value>] [--description <value>] [--seo <json|file>]Update a blog category
blog categories delete <id> --confirmDelete a blog category

Site layout and menu

What frames the blog (Your site → Look and menu, „Wygląd bloga”) and the site's one shared menu, under zanfia site. site layout pages lists the pages that can frame the blog with each one's issue; site layout set page --page <id> gives the blog that page's header, menu and footer, site layout set storefront brings back the storefront shell. site menu set --menu takes the WHOLE menu as a JSON array (inline or a .json file; read site menu get --json first). Every save is live at once and checked by the same rules as the dashboard.

CommandPurpose
site layout getGet the site layout
site layout pagesList pages that can frame the blog
site layout set <kind> [--page <value>]Set what frames the blog
site menu getGet the shared site menu
site menu set --menu <json|file>Set the shared site menu

Site redirects

The site's 301 redirect map (Your site → Redirects): old addresses of a site moved to Zanfia and where they go now. A line is picked by its from; every change is checked by the same rules as the dashboard (no chains, loops or duplicates, at most 500 lines).

CommandPurpose
site-redirects listList site redirects
site-redirects create <from> <to>Add a site redirect
site-redirects update <from> [--new-from <value>] [--to <value>]Change a site redirect
site-redirects delete <from> --confirmRemove a site redirect

Pop-ups

Pop-ups on your site (Your site → Pop-ups): a signup form that opens on entry or after scrolling, after the visitor's cookie decision, as a bottom sheet on phones, and never again after a signup. create makes a draft (a new signup form unless you pass content.formId); status <id> active puts it on the site within a few minutes. Needs the pop-ups feature on your plan.

CommandPurpose
popups listList pop-ups
popups create --name <value> [--priority <n>] [--language <pl|en|default>] [--copy <json|file>] [--content <json|file>] [--design <json|file>] [--trigger <json|file>] [--targeting <json|file>] [--frequency <json|file>] [--schedule <json|file>]Create a pop-up
popups get <id>Pop-up detail
popups update <id> [--name <value>] [--priority <n>] [--language <pl|en|default>] [--copy <json|file>] [--content <json|file>] [--design <json|file>] [--trigger <json|file>] [--targeting <json|file>] [--frequency <json|file>] [--schedule <json|file>]Update a pop-up
popups status <id> <status>Activate or pause a pop-up
popups delete <id> --confirmDelete a pop-up
popups stats <id> [--from <value>] [--to <value>]Pop-up stats

Pages

Custom hosted HTML pages, served on your storefront domain. Statuses: draft, published.

  • pages list
    Hosted pages
  • pages get <id>
    One page, HTML body included
  • pages create --slug <s> --title <t> (--file <path> | --html <h>) [--publish [--allow-placeholders]]
    New page, draft unless --publish; --allow-placeholders publishes even with fields still to fill in
  • pages update <id> [--slug] [--title] [--file | --html] [--no-cookie-banner | --cookie-banner] [--event-start <iso>] [--event-timezone] [--event-duration <min>] [--join-url <url>] [--replay-url <url>] [--live-room <roomId>]
    Edit; --no-cookie-banner hides the workspace cookie banner on this page only (pixels still wait for consent given elsewhere). The --event-* / link options set a webinar page's date and links: a new --event-start moves the reminders, --join-url changes the watch page button at once, none clears a link or detaches the webinar (--live-room needs the live_rooms plan feature)
  • pages publish <id> [--only-this-page] [--dry-run] [--allow-placeholders]
    Go live. A site's home page takes every site page along, drafts too, unless --only-this-page; --dry-run lists what would change; --allow-placeholders publishes even with fields still to fill in
  • pages unpublish <id> [--only-this-page] [--dry-run]
    Back to draft (a site's home page: the whole site, unless --only-this-page)
  • pages delete <id>
    Delete the page and its HTML
  • pages set-main <id>
    Use a published page as the storefront homepage
  • pages unset-main
    Revert the storefront to the product grid
  • pages generate --intent <lead-magnet|webinar|sales-page|thank-you|waitlist|newsletter|challenge|application|upsell|low-ticket|generic> --title <t> --brief <text|file> [--template] [--product] [--checkout] [--form] [--source-url --source-mode rebuild|inspired] [--slug] [--delivery-url] [--delivery-name] [--event-start <iso>] [--event-timezone] [--event-duration <min>] [--event-mode live|on_demand] [--replay-media <mediaId>] [--opt-in-mode single|double] [--form-tags a,b] [--json <file>] --confirm
    AI-generate a draft page (consumes AI credits; 202 → runId, poll pages list). Signup intents also provision a form, a thank-you page and a draft delivery automation; a 503 means the Trigger.dev scheduler is not configured on that environment
  • pages edit <pageId> --instruction <text|file> --confirm
    AI-edit the page's content or design from a described change: synchronous, up to 1-2 minutes; prints the change summary and the revision id of the previous HTML
  • pages templates
    Design templates usable as pages generate --template seeds (id, name, category, intents)
  • pages revisions <pageId>
    HTML snapshots taken before each overwrite (newest first)
  • pages restore <pageId> <revisionId> --confirm
    Roll back to a revision (the current HTML is snapshotted first, so a restore is itself undoable)
  • pages family-step <pageId> --step <follow-ups|thank-you-page|room-page>
    Build one missing piece of a page family (the editor checklist's rows): follow-ups = the reminder ladder of a dated webinar page (e.g. your own page that got its date with pages update --event-start), as drafts that pages activate-automations schedules; thank-you / watch pages are AI generations (credits)
  • pages activate-automations <pageId> --confirm
    Publish the page's draft delivery automation and schedule its webinar follow-up broadcasts: emails send for real; placeholder copy is skipped

Social media

Posts to the connected social accounts (Marketing → Social). Statuses: draft, scheduled, published, partially_failed (some accounts got the post, at least one failed), failed. Scopes: social:read / social:write.

  • social accounts
    Connected accounts and their target keys (linkedin, facebook:{pageId}, …)
  • social posts [--status <s>] [--limit <n>]
    Browse drafts, scheduled posts and publish history
  • social get <id>
    One post: content, media, schedule, publish results (permalinks)
  • social draft --text <t> --targets <a,b> [--media <ids>] [--first-comment <t>]
    Save a draft (appears in the dashboard Posts view)
  • social schedule [id] --at <iso> [--text … --targets … [--media] [--first-comment]]
    Schedule an existing draft or new content (>= 1 min ahead)
  • social publish [id] [--text … --targets linkedin [--media] [--first-comment]]
    Publish now: LinkedIn only (text + images)
  • social update <id> [--text] [--targets] [--media | --clear-media] [--first-comment | --clear-first-comment]
    Edit a draft or scheduled post (a scheduled one stays at its time); published posts can't be edited
  • social unschedule <id>
    Move a scheduled post back to drafts: it is not published at its time
  • social delete <id> --confirm
    Delete a post; a published post stays live on the network, only Zanfia's record goes

Media library

  • media upload --file <path> [--name] [--mime] [--folder] [--description]
    Upload a file; prints the media id and public URL
  • media upload-video --file <path> [--title] [--mime] [--wait] [--poll-timeout <s>]
    Upload a video for lessons (Bunny); transcoding runs after upload
  • media list [--folder <name>]
    Browse
  • media get <id>
    One item; shows uploadStatus
  • media rename <mediaId> [--name] [--description | --clear-description]
    Rename and/or edit the description
  • media move <mediaId...> (--folder <name> | --unfile)
    File items into a folder, or unfile them
  • media transcript <mediaId> [--words] [--out <file>]
    A video transcript (plain text; --words adds word-level timings and implies JSON)
  • media generate-image --prompt <text> --confirm
    Generate an AI image into the media library (spends AI credits)
  • media transcribe <mediaId> --confirm
    Start a video transcription (spends transcription minutes)
  • media delete <id> [--force]
    Delete (--force if still referenced elsewhere)
  • media folder list
    Folders
  • media folder create <name>
    New folder (idempotent by name)
  • media folder rename <name> --to <newName>
    Rename: renaming onto an existing folder merges into it
  • media folder delete <name>
    Delete a folder: its items are unfiled, never deleted

Videos need transcoding

media upload-video without --wait returns while the video is still processing. Poll media get <id> until uploadStatus is ready, then bind it with courses lesson set-video.

Was this article helpful?

Related articles

Spotted something off? Tell us at support@zanfia.com.