Documentation Index
Fetch the complete documentation index at: https://handbook.polar.sh/llms.txt
Use this file to discover all available pages before exploring further.
Status: Active
Created: March 2026
Last Updated: May 2026
Summary
We want to start using Polar as the billing platform for Polar. This will put us closer to where our customers are and make us understand our own product better. Every Polar organization becomes a customer of our own Polar-for-Polar organization, organization users become members on those customers, and event ingestion is metered for possible future usage-based billing. Additionally cost events are ingested for costs incurred to Polar for example onboarding and other costs.Goals
- Use Polar’s own APIs (customers, subscriptions, members, events) to manage our billing relationship with organizations.
- Track organization membership via the Polar member model so members have access to the customer portal.
- Meter event ingestion per organization for future usage-based billing.
- Receive webhooks from Polar to sync state changes back.
- All changes are safe to deploy incrementally — no big bang migration.
- Introduce paid plans for lower transaction fees and better support.
Non-Goals
- Replacing existing internal analytics (Tinybird, PostHog). Metering is for billing, not observability.
Key Concepts
- “Our Polar org”: The Polar organization that owns the customers, plans, and benefits we use to bill everyone else. Configured via
POLAR_ORGANIZATION_ID. external_customer_id: We don’t store a Polar customer ID on the Organization model. Every Polar API call and incoming webhook identifies the customer byexternal_customer_id = str(org.id).external_idon members: User UUIDs becomeexternal_idon Polar members.- Recursion guard: Two places stop the self org from billing itself — the metering cron excludes it when counting events, and
enqueue_track_organization_review_usageshort-circuits when the external customer is the self org.
Architecture
Mapping
| Polar concept | Our concept | Identifier |
|---|---|---|
| Customer | Organization | external_id = str(org.id) |
| Member | User in organization | external_id = str(user.id) |
| Subscription | Paid plan on the organization | Created per organization when they pick a plan. The free tier is synthesized client-side, not a real subscription. |
| Metering event | Batch of ingested events | external_customer_id = str(organization.id) |
Webhooks
To handle changes for the customer in Polar (e.g. purchase, cancelation, etc), the Polar application will listen to webhooks from Polar.Endpoint
Createserver/polar/integrations/polar/endpoints.py:
standardwebhooks library.
ExternalEvent Integration
Addpolar = "polar" to the ExternalEventSource enum and a polymorphic subclass:
Webhook Handlers
Initially handle a minimal set of events, with stubs for future expansion:benefit_grant.* is how paid plans take effect: the benefit’s metadata.type (transaction_fee or support) drives whether we rewrite the org’s Account fees or update its Plain support tier. order.created is what triggers the subscription confirmation / renewal emails with the invoice PDF attached — it’s enqueued with a delay so the invoice has time to generate.
API Surface
All endpoints are added as private endpoints (APITag.private) on the /organizations router, scoped by {id} and authorized against the calling user’s access to that organization. They go via PolarSelfService to the SDK client. They are essentially thin wrappers around the external APIs that are calling Polar but in the context of the Polar-for-Polar organization and the customer session for the specific customer.
Plans & subscription
GET /organizations/{id}/plans— list available paid plansGET /organizations/{id}/subscription— current subscription (or the synthesized free plan)POST /organizations/{id}/subscription— start a checkout for a planPATCH /organizations/{id}/subscription— change plan, with proration depending on directionDELETE /organizations/{id}/subscription— cancel at period end
GET /organizations/{id}/orders— list orders for the customerGET /organizations/{id}/orders/{order_id}/invoice— get an invoice download URL
GET /organizations/{id}/billing-detailsPATCH /organizations/{id}/billing-details
GET /organizations/{id}/payment-methodsDELETE /organizations/{id}/payment-methods/{payment_method_id}POST /organizations/{id}/payment-methods/{payment_method_id}/default— set default
POST /organizations/{id}/customer-session— short-lived token for the customer portal endpoints
POST /integrations/polar/webhook) is on its own router and is excluded from the OpenAPI schema. See the Webhooks section.
Subscriptions
The different paid plans are set up with benefits granting them lower fees, setting a support tier, or anything else that might be part of a paid plan. On the webhook of the benefit.grant that data is updated as described in the prior section. The free plan was initially set to be a subscription as well, so that all customers were on a subscription but this was scrapped for two primary reasons:- We were granting a lot of benefits unnecessarily which overwhelmed the system.
- If everyone is always on a subscription it makes it more difficult to track subscription growth and cancellations.
Meter
Initial plan
The initial plan was to act on every ingested event while still bundling them. So if an organization would ingest 200 events via POST /v1/events/ingest, we would then create a single event calledevent_ingestion with the metadata { "count": 24 }. If they only ingested a single event it would be 1:1 between the ingest call and the ingest call toward Polar for Polar.
However this quickly spiraled out of control since every ingest call triggers the event.ingested task, which in turn triggers customer_meter.update_customer and tinybird.ingest as well as the new polar_self.track_event_ingestion. This meant that every event ingestion potentially triggered 14+ tasks.
Alternative suggestion
Two alternative suggestions were raised:- Use debounce on
polar_self.track_event_ingestionso that we don’t trigger new tracking tasks until the last one has been processed. - Have a cron job that goes through the events ingested since last ingestion and trigger a
polar_self.track_event_ingestionfor those.
Implementation
The implementation will happen in multiple steps:- Config + Integration Module — Add POLAR_* settings, move polar-sdk to regular deps, and scaffold a PolarSelfService singleton with no-op methods that early-return when unconfigured. No behavioral change.
- Customer on Org Signup — On organization creation, enqueue a background task to create a Polar customer (keyed by org ID). Includes a backfill script for existing orgs. (We initially also created a free-tier subscription here, but dropped it — the free plan is synthesized client-side instead.)
- Members — Sync UserOrganization add/remove to Polar members on the customer via background tasks. Backfill extended to cover existing memberships.
- Metering Event Ingestion — Hook the ingested events to be bundled and ingested into into our own Polar organization with a count for the number of events ingested.
- Receive Polar Webhooks — Add /integrations/polar/webhook endpoint with signature verification, plus a polar ExternalEventSource enum value and handlers for benefit_grant.* and order.created events.
- Paid Plans — Wire checkout, plan changes (with proration), cancellation, the customer portal proxy methods (payment methods, billing details, customer session), and order/invoice listing. Benefits drive fees and support tier via the webhook handler in step 5.

