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.
Boundary: Polar vs gateway
Section titled “Boundary: Polar vs gateway”| Action | Where | Notes |
|---|---|---|
| Create / update / archive Polar products | Direct Polar API | Each 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 checkout | Gateway, never direct | POST /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 webhooks | Always via gateway | Webhook 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. |
| Refunds | Gateway (when implemented) — Polar dashboard for now | The 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. |
Naming on Polar
Section titled “Naming on Polar”Format: <product-slug>:<plan> — lowercase, dash-separated.
Examples:
fotoyu:pro,fotoyu:powerhrdex:t1-monthly,hrdex:t1-yearly,hrdex:t2-monthly,hrdex:t2-yearlyquay:monthly,quay:yearly- ❌
book:lite-engagement— don’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.
Required Polar product metadata
Section titled “Required Polar product metadata”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.
Pricing model conventions
Section titled “Pricing model conventions”- One-time — for licenses, lifetime upgrades, etc. Use
recurring_interval: null. - Recurring monthly —
recurring_interval: "month". Default for SaaS subscriptions. - Recurring annual —
recurring_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.
Sandbox parity
Section titled “Sandbox parity”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 .envPOLAR_PRODUCT_PRO_SANDBOX=8250c6c9-54e3-4735-a25a-d4740b79b552POLAR_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).
Token & secret storage convention
Section titled “Token & secret storage convention”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 600Don’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.
When NOT to put a product on Polar
Section titled “When NOT to put a product on Polar”- 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.