Skip to content

Polar product naming

When wiring a new uncle-z product to Polar, follow these conventions so the org catalogue stays scannable, billing rolls up cleanly per product, and the gateway’s webhook routing keeps working without per-product code changes.

ActionWhereNotes
Create / update / archive Polar productsDirect Polar APIEach product team can hit https://api.polar.sh/v1/products (or sandbox host) with their own OAT. The gateway does NOT proxy product CRUD — it would add a layer with no real isolation benefit since all uncle-z products share one Polar org anyway.
Issue a checkoutGateway, never directPOST /v1/payments method=polar metadata.polar_product_id=<polar uuid>. The gateway threads gateway_payment_id into Polar’s metadata so inbound webhooks route back to the right gateway payment row. Direct checkouts bypass that and orphan in the gateway.
Receive Polar webhooksAlways via gatewayWebhook URL is https://payment.uncle-z.com/webhooks/polar (registered once per org, in Polar’s dashboard). The gateway verifies signatures, updates the canonical payment row, and forwards to your product’s webhook_url with the gateway’s HMAC scheme. Don’t register a per-product webhook on Polar — there’s exactly one secret per org and it’s owned by the gateway.
RefundsGateway (when implemented) — Polar dashboard for nowThe gateway’s polar driver doesn’t yet implement programmatic refunds. Until it does: refund manually from Polar’s dashboard. The gateway picks up the order.refunded webhook and updates its row + forwards refund.succeeded.

Format: <product-slug>:<plan> — lowercase, dash-separated.

Examples:

  • fotoyu:pro, fotoyu:power
  • hrdex:t1-monthly, hrdex:t1-yearly, hrdex:t2-monthly, hrdex:t2-yearly
  • quay:monthly, quay:yearly
  • book:lite-engagementdon’t. book.uncle-z.com is services, AUP-rejected by Polar. Book stays on PayPal.

The <product-slug> should match the gateway app name where possible (e.g. quay-sandbox / quay-live apps → quay:* Polar products) so dashboards align. If a product needs a marketing-friendly buyer-facing name, set Polar’s description field to that — buyers see name+description on the checkout page.

Set these on the Polar product so we can audit which uncle-z product owns which Polar entry:

{
"uncle_z_product": "fotoyu",
"uncle_z_plan": "pro",
"owner_email": "team-fotoyu@uncle-z.com"
}

Why: there’s no per-product Polar org separation, so this metadata is how we tell apart whose products are whose during cleanup, audit, or migration.

  • One-time — for licenses, lifetime upgrades, etc. Use recurring_interval: null.
  • Recurring monthlyrecurring_interval: "month". Default for SaaS subscriptions.
  • Recurring annualrecurring_interval: "year". Optional alongside monthly; products may offer both.
  • Free tier — don’t model on Polar; gate at the product level. Polar charges per-transaction, not per-product-listing — but free tiers don’t transact.
  • Tiered / volume / metered — talk to ops before building. Polar supports it but the gateway driver hasn’t been tested against those shapes.

Currency: USD by default. EUR / GBP only if a product has real demand — every currency adds a tax-config surface to maintain.

Every product created on live should also exist on sandbox with the SAME name field, so smoke tests catch shape regressions before they hit live.

The Polar product UUIDs differ between sandbox and live (separate orgs). Each product needs to maintain BOTH ids in its config:

# fotoyu .env
POLAR_PRODUCT_PRO_SANDBOX=8250c6c9-54e3-4735-a25a-d4740b79b552
POLAR_PRODUCT_PRO_LIVE=...

The product passes the right one to the gateway based on which mode it’s currently running in (which app credentials are loaded).

Each product owns its Polar tokens (per-product OATs at polar.sh / sandbox.polar.sh, scoped read + products:write + custom_fields:write). Store outside the repo:

~/.config/<product>/polar-oat-sandbox.txt # chmod 600
~/.config/<product>/polar-oat-live.txt # chmod 600

Don’t commit either. Don’t share across products. Rotate at the issuing dashboard.

The gateway has its own runtime token in prod env (POLAR_LIVE_ACCESS_TOKEN) — separate from product-side tokens and not your concern.

  • It’s a service. Polar’s AUP rejects services (per-hour dev work, design retainers, consulting). Use PayPal instead.
  • Buyers are exclusively Indonesian. Use IDR rails (NICEPAY) — Polar is for global / non-IDR flows.
  • You need a custom payment form. Polar’s hosted checkout is fixed. If you need an embedded card form, that’s PayPal’s advanced flow or a future Stripe integration — talk to ops.