Polar Billing Webhook Flow
skyflow intermediate 6 min read
ELI5
Skyflow doesn’t process payments — Polar.sh does. Skyflow creates a checkout session, redirects the user, and then waits for Polar to call POST /webhooks/polar. The webhook handler verifies the HMAC signature, upserts the subscriptions row, and re-publishes the event onto NATS so the rest of the system reacts.
Technical Deep Dive
Checkout Initiation
sequenceDiagram autonumber participant C as Client participant API as core-api participant P as Polar API participant PG as Postgres
C->>API: POST /api/v1/billing/checkout {tier, interval} API->>API: assert user not already on tier API->>P: create checkout session P-->>API: {checkout_url, session_id, expires_at} API->>PG: INSERT checkout_sessions (polar_session_id UNIQUE, status='pending') API-->>C: {checkout_url, session_id, expires_at} C->>P: browser → Polar hosted checkoutWebhook Sequence
sequenceDiagram autonumber participant P as Polar participant API as core-api /webhooks/polar participant PG as Postgres participant N as NATS SUBSCRIPTION participant U as USER stream
P->>API: POST signed payload API->>API: verify HMAC (POLAR_WEBHOOK_SECRET) alt signature invalid API-->>P: 401 else valid API->>API: parse event_type alt checkout.completed API->>PG: UPDATE checkout_sessions SET status='completed', completed_at=now() API->>PG: INSERT subscriptions (polar_subscription_id UNIQUE, status='active') API->>N: publish events.subscription.created API->>U: publish events.user.tier_changed (UserTierChangedEvent) else subscription.active API->>PG: UPDATE users SET tier=:new_tier else subscription.canceled API->>PG: UPDATE subscriptions SET cancel_at = current_period_end Note over API,PG: 7-day grace, downgrade scheduled at period_end API->>N: publish events.subscription.canceled else subscription.payment_failed API->>N: publish events.subscription.payment_failed Note over API: triggers events.realtime.tier_limit_warning later end API-->>P: 200 {received: true} endIdempotency Surfaces
| Layer | Mechanism |
|---|---|
subscriptions.polar_subscription_id | UNIQUE — ON CONFLICT update only |
checkout_sessions.polar_session_id | UNIQUE |
| Webhook re-delivery | Polar retries on non-2xx; HMAC + UNIQUE keys keep it safe |
| Downstream NATS | BaseEvent.event_id consumers dedupe |
Cancellation Grace
subscription.canceled does not flip status to canceled immediately. It sets cancel_at = current_period_end. A scheduled job downgrades the user only after current_period_end passes (the spec calls this a 7-day grace window). The partial unique index one_active_subscription_per_user allows a future new active subscription once the row flips.
Failure Flow
flowchart TD P[Polar fires webhook] --> V{HMAC valid?} V -->|No| L[401 + log] V -->|Yes| H[handler] H --> R{Postgres write OK?} R -->|No| E[500 → Polar will retry] R -->|Yes| N[publish to NATS] N --> A[200 ack]Key Terms
- HMAC (Polar) → header signature derived from
POLAR_WEBHOOK_SECRET; mandatory verification before any DB write polar_subscription_idUNIQUE → idempotency anchor; replays are safe upserts, not duplicates- Cancellation grace → cancel-at-period-end; service continues until billing period ends
UserTierChangedEvent→ secondary event so non-billing services (gamification, realtime) update their views
Q&A
Q: How is the webhook authenticated?
A: HMAC signature in a Polar-provided header, verified against POLAR_WEBHOOK_SECRET. Failure returns 401 before any state mutation.
Q: What happens when Polar retries a webhook?
A: The unique constraints on polar_subscription_id and polar_session_id make the writes idempotent. Downstream NATS consumers further dedupe on BaseEvent.event_id.
Q: Why doesn’t subscription.canceled immediately downgrade the user?
A: The user has paid through current_period_end. A scheduled job demotes the tier only after that timestamp; meanwhile they keep their paid features.
Q: What does the user see if their card declines mid-cycle?
A: events.subscription.payment_failed triggers an email (subscription-notifier) and queues events.realtime.tier_limit_warning for the in-app notification surface; the tier is not flipped until the grace policy decides.
Examples
Like Netflix billing. You click “subscribe” (checkout session), enter your card on Stripe’s hosted page (Polar), and Stripe pings Netflix’s back end (“payment OK”), at which point your account flips to premium. If you cancel, Netflix lets you watch until the month ends; only then does the badge disappear.
neighbors on the map
- Billing Architecture debugging why a user's tier did not update after payment
- Golden Ticket Issuance auditing the five-ticket cap enforcement