--- id: fcakyon/claude-codex-settings/polar-billing version: "316272b7" license: Apache-2.0 install: manual updated: 2026-07-26 --- # polar-billing — Polar Billing is a reference for Polar's complete billing infrastructure, covering core entities like checkouts, orders, subscriptions, and transactions alongside their relationships and data flows. It details Stripe integration, subscription lifecycle management including trials and cancellations, proration logic, and benefit grants. The skill maps key services, background task handling, dunning retry strategies, and transaction ledger mechanics to help developers navigate payment workflows end-to-end. Publisher: fcakyon · Stars: 820 · Updated: 2026-07-26 Install (manual): `git clone https://github.com/fcakyon/claude-codex-settings` ## SKILL.md # Polar Billing System Comprehensive guide to Polar's billing infrastructure, covering entities, flows, Stripe integration, and benefit provisioning. ## Quick Reference ``` Checkout → Payment → Order → Transaction → Benefits ↓ Subscription (if recurring) ↓ Subscription Cycle → Order → ... ``` ## Table of Contents 1. [Core Entities](#1-core-entities) 2. [Entity Relationships](#2-entity-relationships) 3. [Main Services](#3-main-services) 4. [Dramatiq Background Tasks](#4-dramatiq-background-tasks) 5. [Stripe Integration](#5-stripe-integration) 6. [Subscription Lifecycle](#6-subscription-lifecycle) 7. [Proration System](#7-proration-system) 8. [Benefits & Credits](#8-benefits--credits) 9. [Dunning & Payment Retry](#9-dunning--payment-retry) 10. [Transaction Ledger](#10-transaction-ledger) 11. [Key File Locations](#11-key-file-locations) --- ## 1. Core Entities ### Checkout **File:** `server/polar/models/checkout.py` Shopping cart/payment session before order confirmation. | Field | Type | Description | |-------|------|-------------| | `status` | CheckoutStatus | open, expired, confirmed, succeeded, failed | | `payment_processor` | PaymentProcessor | stripe, manual | | `client_secret` | str | Unique identifier for frontend | | `amount`, `currency` | int, str | Price in cents | | `tax_amount`, `discount_amount` | int | Calculated amounts | | `allow_trial`, `trial_end` | bool, datetime | Trial configuration | | `seats` | int | For seat-based products | **Relationships:** organization, customer, product, product_price, discount, subscription (for upgrades) --- ### CheckoutLink **File:** `server/polar/models/checkout_link.py` Persistent URL that creates Checkout Sessions on visit. | Field | Type | Description | |-------|------|-------------| | `client_secret` | str | Unique identifier for the URL | | `seats` | int \| None | Preconfigured seat count for seat-based pricing | | `discount_id` | UUID \| None | Preset discount to apply | | `trial_interval`, `trial_interval_count` | Trial config | Override product trial settings | **Relationships:** organization, products, discount --- ### Order **File:** `server/polar/models/order.py` Represents a billing event (one-time purchase or subscription cycle). | Field | Type | Description | |-------|------|-------------| | `status` | OrderStatus | pending, paid, refunded, partially_refunded | | `billing_reason` | OrderBillingReason | purchase, subscription_create, subscription_cycle, subscription_update | | `subtotal_amount` | int | Amount before discount/tax | | `discount_amount` | int | Discount applied | | `tax_amount` | int | Tax collected | | `applied_balance_amount` | int | Account balance applied | | `platform_fee_amount` | int | Polar's fee | | `refunded_amount` | int | Already refunded | | `next_payment_attempt_at` | datetime | Dunning retry time | **Computed Properties:** - `net_amount` = subtotal - discount - `total_amount` = net + tax - `due_amount` = max(0, total + applied_balance) - `payout_amount` = net - platform_fee - refunded --- ### Subscription **File:** `server/polar/models/subscription.py` Recurring billing relationship. | Field | Type | Description | |-------|------|-------------| | `status` | SubscriptionStatus | incomplete, trialing, active, past_due, canceled, unpaid | | `amount`, `currency` | int, str | Subscription gross price | | `net_amount` | int | Net amount (gross minus inclusive tax, equal to gross if tax-exclusive) | | `tax_behavior` | TaxBehavior \| None | Inclusive, exclusive, or null (set at creation) | | `recurring_interval` | Interval | month, year | | `current_period_start/end` | datetime | Billing period | | `trial_start/end` | datetime | Trial period | | `cancel_at_period_end` | bool | Scheduled cancellation | | `canceled_at`, `ended_at` | datetime | Lifecycle timestamps | | `past_due_at` | datetime | When payment failed | | `seats` | int | For seat-based pricing | **Relationships:** organization, customer, product, payment_method, discount, meters, grants (benefits) --- ### Transaction **File:** `server/polar/models/transaction.py` All money flows in the system. | Field | Type | Description | |-------|------|-------------| | `type` | TransactionType | payment, processor_fee, refund, dispute, balance, payout | | `processor` | Processor | stripe, manual | | `amount`, `currency` | int, str | Transaction amount | | `tax_amount` | int | Tax portion | **Self-referential relationships:** payment_transaction, balance_transactions, incurred_transactions --- ### Payment **File:** `server/polar/models/payment.py` Individual payment transaction. | Field | Type | Description | |-------|------|-------------| | `status` | PaymentStatus | pending, succeeded, failed | | `processor_id` | str | Stripe charge ID | | `method` | str | card, bank_transfer, etc. | | `trigger` | PaymentTrigger \| None | What initiated payment: purchase, subscription_cycle, retry_dunning, retry_customer, retry_payment_method_update, retry_admin | | `decline_reason` | str | Why payment failed | | `risk_level`, `risk_score` | str, int | Fraud assessment | --- ### Refund **File:** `server/polar/models/refund.py` | Field | Type | Description | |-------|------|-------------| | `status` | RefundStatus | pending, succeeded, failed, canceled | | `reason` | RefundReason | duplicate, fraudulent, customer_request, etc. | | `amount`, `tax_amount` | int | Refund amounts | | `revoke_benefits` | bool | Whether to revoke customer benefits | --- ### Customer **File:** `server/polar/models/customer.py` | Field | Type | Description | |-------|------|-------------| | `email`, `name` | str | Contact info | | `billing_name` | str | Name for invoices (falls back to `name`) | | `stripe_customer_id` | str | Stripe link | | `billing_address` | Address | Stored address | | `tax_id` | str | For tax compliance | --- ### Product & ProductPrice **Files:** `server/polar/models/product.py`, `server/polar/models/product_price.py` | ProductPrice Types | Description | |-------------------|-------------| | `ProductPriceFixed` | Fixed amount (set `price_amount=0` for free) | | `ProductPriceCustom` | Merchant sets at checkout | | `ProductPriceMeteredUnit` | Pay-per-unit | | `ProductPriceSeatUnit` | Per-seat with tiers | --- ### BillingEntry **File:** `server/polar/models/billing_entry.py` Audit log for billing calculations. | Field | Type | Description | |-------|------|-------------| | `type` | BillingEntryType | cycle, proration, metered, seats_increase, seats_decrease | | `direction` | Direction | debit, credit | | `amount` | int | Entry amount | --- ## 2. Entity Relationships ``` Organization ├── Product │ ├── ProductPrice (multiple per product) │ └── ProductBenefit → Benefit ├── Customer │ ├── Subscription → Organization, Product, Discount │ │ ├── SubscriptionProductPrice │ │ ├── SubscriptionMeter │ │ └── BenefitGrant │ ├── Order → Product, Subscription │ │ └── OrderItem │ ├── PaymentMethod │ └── Wallet ├── Checkout → Customer, Product ├── Discount │ └── DiscountRedemption └── Account (for payouts) └── Payout → Transaction Transaction (ledger) ├── payment → Order, Customer ├── refund → Refund, Order ├── dispute → Dispute, Order ├── processor_fee → parent payment └── payout → Account ``` --- ## 3. Main Services ### SubscriptionService **File:** `server/polar/subscription/service.py` Core subscription operations: ```python # Creation create_or_update_from_checkout(checkout, payment_method) → (Subscription, created) # Updates update_product(subscription, product_id, proration_behavior, discount=None) update_seats(subscription, seats, proration_behavior) update_discount(subscription, discount) # discount: UUID | Literal["unset"] update_trial(subscription, trial_end) # Lifecycle cycle(subscription) # Period renewal cancel(subscription) # At period end revoke(subscription) # Immediately uncancel(subscription) # Benefits enqueue_benefits_grants(task="grant"|"revoke", customer, product) ``` ### OrderService **File:** `server/polar/order/service.py` ```python create_from_checkout(checkout) # One-time purchase create_subscription_order(subscription, billing_reason) # Recurring trigger_payment(order) # Charge customer create_order_balance(order) # Ledger entries ``` ### CheckoutService **File:** `server/polar/checkout/service.py` ```python create(product, customer_data, discount_code) confirm(checkout) # Lock checkout for payment handle_stripe_success(checkout, charge) handle_free_success(checkout) # No payment needed ``` ### PaymentService **File:** `server/polar/payment/service.py` ```python upsert_from_stripe_charge(charge, checkout, order) handle_success(payment) # Complete order handle_failure(payment) # Update order status ``` ### RefundService **File:** `server/polar/refund/service.py` ```python create(order, amount, reason, revoke_benefits) upsert_from_stripe(stripe_refund) # Also enqueues chargeback prevention notice for dispute_prevention refunds ``` ### BenefitGrantService **File:** `server/polar/benefit/grant/service.py` ```python enqueue_benefits_grants(task, customer, product, order=None, subscription=None) grant_benefit(customer, benefit) revoke_benefit(customer, benefit) ``` --- ## 4. Dramatiq Background Tasks ### Subscription Tasks **File:** `server/polar/subscription/tasks.py` | Task | Trigger | Action | |------|---------|--------| | `subscription.cycle` | Scheduler at period end | Renew subscription, create order | | `subscription.update_product_benefits_grants` | Product benefits changed | Update all grants | | `subscription.cancel_customer` | Customer deleted | Cancel all billable subscriptions (trialing, active, past_due) | ### Order Tasks **File:** `server/polar/order/tasks.py` | Task | Trigger | Action | |------|---------|--------| | `order.create_subscription_order` | Subscription cycle | Create billing order | | `order.trigger_payment` | Order ready | Charge payment method | | `order.balance` | Payment success | Create ledger entries | | `order.invoice` | Order created | Generate PDF invoice | | `order.process_dunning` | **Hourly cron** | Find orders for retry | | `order.process_dunning_order` | Individual retry | Retry single payment | ### Stripe Webhook Tasks **File:** `server/polar/integrations/stripe/tasks.py` | Task | Stripe Event | Action | |------|--------------|--------| | `charge.succeeded` | Payment complete | Create order, provision benefits | | `charge.failed` | Payment failed | Mark order failed | | `charge.updated` | Charge settled | Create ledger transaction | | `refund.created/updated` | Refund processed | Update refund record | | `charge.dispute.created` | Chargeback | Create dispute, revoke benefits | | `payout.paid` | Payout complete | Update payout status | ### Benefit Tasks **File:** `server/polar/benefit/tasks.py` | Task | Trigger | Action | |------|---------|--------| | `benefit.enqueue_benefits_grants` | Order/subscription | Queue individual grants | | `benefit.grant` | Individual benefit | Provision access (GitHub, Discord, etc.) | | `benefit.revoke` | Cancellation/refund | Remove access | | `benefit.cycle` | Subscription renewal | Reset credits with rollover | ### Checkout Tasks **File:** `server/polar/checkout/tasks.py` | Task | Trigger | Action | |------|---------|--------| | `checkout.handle_free_success` | Free product | Complete without payment | | `checkout.expire_open_checkouts` | **Every 15 min** | Mark expired checkouts | ### Payout Tasks **File:** `server/polar/payout/tasks.py` | Task | Trigger | Action | |------|---------|--------| | `payout.trigger_stripe_payouts` | **Daily 00:15 UTC** | Initiate pending payouts | | `payout.created` | Payout created | Event hook (fires for held payouts too) | | `payout.transfer` | After payout.created | Stripe transfer (skipped for held) | | `payout.release_held_payouts` | Org approved | Move held → pending, enqueue transfers | | `payout.cancel_account_payouts` | Org denied/blocked/offboarding | Cancel held+pending payouts | | `payout.cancel_held_payouts` | Payout account swap | Cancel only held payouts on old account | ### Refund Tasks **File:** `server/polar/refund/tasks.py` | Task | Trigger | Action | |------|---------|--------| | `refund.send_chargeback_prevention_notice` | Dispute prevention refund created | Email org owners/admins about refund | --- ## 5. Stripe Integration ### Webhook Endpoints **File:** `server/polar/integrations/stripe/endpoints.py` - `/v1/integrations/stripe/webhook` - Direct webhooks - `/v1/integrations/stripe/webhook-connect` - Connect account webhooks ### Implemented Webhooks **Payment Flow:** - `payment_intent.succeeded` - Payment complete - `payment_intent.payment_failed` - Payment failed - `setup_intent.succeeded` - Card saved - `charge.pending/failed/succeeded/updated` - Charge lifecycle **Refunds:** - `refund.created/updated/failed` **Disputes:** - `charge.dispute.created/updated/closed` **Connect:** - `account.updated` - Account info changed - `payout.updated/paid` - Payout lifecycle ### Webhook Processing Flow ``` Stripe POST → Verify signature → ExternalEvent.enqueue() ↓ Store in external_events table ↓ Enqueue Dramatiq task ↓ Worker processes async ↓ Mark handled_at on success ``` ### StripeService **File:** `server/polar/integrations/stripe/service.py` Key methods: - `create_payment_intent()`, `create_setup_intent()` - `create_refund()`, `get_refund()` - `create_tax_calculation()`, `create_tax_transaction()` - `transfer()`, `create_payout()` --- ## 6. Subscription Lifecycle ### Creation Flow ``` 1. Checkout created (status=open) 2. Customer completes payment 3. Stripe charge.succeeded webhook 4. payment.handle_success() called 5. checkout_service.handle_stripe_success() 6. subscription_service.create_or_update_from_checkout() - Creates Subscription (status=active or trialing) - Sets billing period - Applies discount - Resets meters 7. Enqueue benefit grants 8. Send confirmation email ``` ### Cycle Flow (Renewal) ``` 1. APScheduler triggers at period end 2. subscription.cycle task runs 3. subscription_service.cycle() - Check cancel_at_period_end - If true: set status=canceled, revoke benefits - If false: advance period dates, check discount expiry 4. Create billing entry (type=cycle) 5. Enqueue order.create_subscription_order 6. Order created with billing_reason=subscription_cycle 7. Enqueue order.trigger_payment 8. Stripe charges payment method 9. charge.succeeded → ledger entries → benefits renewed ``` ### Cancellation Flow **At Period End:** ```python subscription_service.cancel(subscription) # Sets cancel_at_period_end=True, ends_at=current_period_end # Benefits remain until period ends # On next cycle: status=canceled, benefits revoked ``` **Immediately:** ```python subscription_service.revoke(subscription) # Sets status=canceled, ended_at=now # Benefits revoked immediately # Seats canceled if seat-based ``` ### Trial Flow ``` 1. Checkout with trial_end set 2. Subscription created with status=trialing 3. No payment during trial 4. At trial_end, cycle task runs 5. Status transitions to active 6. Order created with billing_reason=subscription_cycle_after_trial 7. First payment charged ``` --- ## 7. Proration System ### When Prorations Occur 1. **Product change** - Upgrade/downgrade to different tier 2. **Seat change** - Add/remove seats 3. **Interval change** - Monthly to yearly ### Proration Calculation ```python # Calculate time remaining in period pct_remaining = (period_end - now) / (period_end - period_start) # Old product credit (what they paid but won't use) old_credit = old_price * old_pct_remaining # New product debit (what they owe for remainder) new_debit = new_price * new_pct_remaining # Net proration net = new_debit - old_credit ``` ### Proration Behaviors | Behavior | Action | |----------|--------| | `prorate` | Add to next invoice | | `invoice` | Create order immediately | ### BillingEntry for Prorations ```python # Credit entry (old product) BillingEntry( type=BillingEntryType.proration, direction=BillingEntryDirection.credit, amount=prorated_old_amount ) # Debit entry (new product) BillingEntry( type=BillingEntryType.proration, direction=BillingEntryDirection.debit, amount=prorated_new_amount ) ``` ### Seat Proration ```python # Adding 2 seats at $10/seat with 50% time remaining delta_amount = 2 * $10 * 0.5 = $10 BillingEntry( type=BillingEntryType.subscription_seats_increase, direction=BillingEntryDirection.debit, amount=1000 # cents ) ``` --- ## 8. Benefits & Credits ### Benefit Types | Type | Description | Grant Action | |------|-------------|--------------| | `meter_credit` | Usage allowances | Create meter_credited event | | `github_repository` | Repo access | Add to GitHub team | | `discord` | Server role | Assign Discord role | | `license_keys` | License distribution | Generate key | | `downloadables` | File access | Grant download permission | | `slack_shared_channel` | Slack Connect channel | Create/invite to shared channel | | `feature_flag` | Feature toggle (API-only) | None — merchant reads via API | | `custom` | Customer-visible note | None — displayed in customer portal | ### Benefit Grant Flow ``` 1. Order/Subscription created 2. enqueue_benefits_grants(task="grant") 3. For each benefit in product: - Skip if already granted - Enqueue benefit.grant task 4. benefit.grant task: - Get/create BenefitGrant record - Call strategy.grant() (type-specific) - Set granted_at - Store properties - Send webhook ``` ### Benefit Revocation Flow ``` 1. Subscription canceled or order refunded 2. enqueue_benefits_grants(task="revoke") 3. For each granted benefit: - Enqueue benefit.revoke task 4. benefit.revoke task: - Call strategy.revoke() (type-specific) - Set revoked_at - Send webhook ``` ### Meter Credits **Grant:** ```python # Create event with units Event(type="meter_credited", units=100) # Update CustomerMeter ``` **Cycle (renewal):** ```python # Calculate rollover rollover = min(remaining_units, rollover_limit) # Reset meter Event(type="meter_reset") # Credit new period + rollover Event(type="meter_credited", units=base_units + rollover) ``` **Revoke:** ```python # Negative credit event Event(type="meter_credited", units=-remaining_units) ``` ### Grace Period Organizations can configure `benefit_revocation_grace_period` (days) to delay benefit revocation for `past_due` subscriptions. --- ## 9. Dunning & Payment Retry ### Dunning Process ``` 1. order.process_dunning runs hourly 2. Finds orders where next_payment_attempt_at <= now 3. For each order: - Enqueue order.process_dunning_order 4. process_dunning_order: - Get customer's payment method - Attempt payment via Stripe - On success: mark order paid - On failure: schedule next attempt ``` ### Retry Schedule Configured in organization settings. Typical pattern: - Day 1: First failure - Day 3: Retry 1 - Day 5: Retry 2 - Day 7: Final retry, then mark unpaid ### Subscription Status During Dunning ``` payment fails → status=past_due, past_due_at=now ↓ benefits may continue (grace period) ↓ retry succeeds → status=active ↓ retry fails → status=unpaid, benefits revoked ``` --- ## 10. Transaction Ledger ### Transaction Types | Type | Description | |------|-------------| | `payment` | Customer payment received | | `processor_fee` | Stripe fees | | `refund` | Money returned to customer | | `refund_reversal` | Refund failed/reversed | | `dispute` | Chargeback loss | | `dispute_reversal` | Won dispute | | `balance` | Internal balance transfer | | `payout` | Money sent to creator | ### Creating Payment Transactions ``` 1. charge.updated webhook (charge settled) 2. Get balance_transaction from Stripe 3. Extract settlement amount and fees 4. Create Transaction(type=payment) 5. Enqueue processor_fee.create_payment_fees 6. Create Transaction(type=processor_fee) ``` ### Payout Flow ``` 1. Creator has balance from transactions 2. payout.trigger_stripe_payouts (daily) 3. Calculate available balance 4. Create Payout record - ACTIVE/OFFBOARDED org: status=pending, enqueue payout.created + payout.transfer - REVIEW/SNOOZED org: status=held, enqueue payout.created only 5. stripe_service.transfer() to Connect account (skipped for held) 6. stripe_service.create_payout() to bank 7. payout.paid webhook → update status Held payout lifecycle: - When org approved: payout.release_held_payouts → status=pending, enqueue transfer - When org denied/blocked/offboarding: payout.cancel_account_payouts → cancel + refund - When payout account swapped: payout.cancel_held_payouts (old account only) ``` --- ## 11. Key File Locations ### Models ``` server/polar/models/ ├── checkout.py ├── order.py ├── order_item.py ├── subscription.py ├── subscription_product_price.py ├── transaction.py ├── payment.py ├── refund.py ├── dispute.py ├── payout.py ├── customer.py ├── product.py ├── product_price.py ├── discount.py ├── benefit.py ├── benefit_grant.py └── billing_entry.py ``` ### Services ``` server/polar/ ├── subscription/service.py ├── order/service.py ├── checkout/service.py ├── payment/service.py ├── refund/service.py ├── dispute/service.py ├── payout/service.py ├── benefit/ │ ├── service.py │ ├── grant/service.py │ └── strategies/ │ ├── meter_credit/service.py │ ├── github_repository/service.py │ ├── discord/service.py │ └── ... └── transaction/service/ ├── payment.py ├── refund.py └── dispute.py ``` ### Background Tasks ``` server/polar/ ├── subscription/tasks.py ├── order/tasks.py ├── checkout/tasks.py ├── benefit/tasks.py ├── payout/tasks.py ├── refund/tasks.py └── integrations/stripe/tasks.py ``` ### Stripe Integration ``` server/polar/integrations/stripe/ ├── endpoints.py # Webhook handlers ├── service.py # Stripe API wrapper ├── tasks.py # Webhook processing tasks └── payment.py # Payment resolution helpers ``` --- ## Common Debugging Scenarios ### Payment Failed 1. Check `Payment` record for `decline_reason` 2. Check `Order.status` and `next_payment_attempt_at` 3. Look at external_events for Stripe webhook ### Benefits Not Granted 1. Check `BenefitGrant` record for errors 2. Look at benefit.grant task in Dramatiq logs 3. Verify product has benefits attached ### Proration Issues 1. Check `BillingEntry` records for subscription 2. Verify billing_reason on Order 3. Check subscription's current_period dates ### Subscription Not Cycling 1. Check `scheduler_locked_at` on subscription 2. Verify APScheduler is running 3. Check subscription.cycle task logs [View on SkillFed](https://skillfed.io/fcakyon/claude-codex-settings/polar-billing) · [View on GitHub](https://github.com/fcakyon/claude-codex-settings)