> ## Documentation Index
> Fetch the complete documentation index at: https://polar.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Implementing Seat-Based Pricing

> Complete guide to implementing team products with seat-based pricing

Seat-based pricing lets you sell products where one person buys seats and assigns them to their team. Each seat holder gets their own benefits — license keys, Discord roles, file downloads, or anything else you offer.

<Warning>
  Seat-based pricing is currently in beta. Enable it under Settings → General → Features.
</Warning>

## Understanding the model

Before writing any code, there are three entities you need to understand. They change how you think about every API call, webhook, and portal flow.

### Customer, Member, and CustomerSeat

With standard Polar products, one person buys and one person uses — they're the same person. With seat-based products, buying and using are separate concerns, modeled through three distinct entities:

* A **Customer** is the billing entity — who pays. They own subscriptions, orders, and payment methods. On first seat-based purchase, the customer is permanently upgraded to `type: "team"`, which enables members and team management.
* A **Member** is a person under a customer — who uses. Each member has their own email, role (`owner`, `billing_manager`, or `member`), and receives benefit grants independently. The person who purchases the product is created as an `owner` member. Both `owner` and `billing_manager` roles can manage seats, update or cancel the subscription, and manage payment methods. The `owner` role may receive additional management capabilities in the future.
* A **CustomerSeat** is the link between a product and a member. It tracks assignment status (`pending`, `claimed`, `revoked`), holds the invitation token, and carries optional metadata.

```
Customer (Jane — purchaser, type: "team")
  ├── Member: Jane (role: owner)          → manages team, can self-assign a seat for benefits
  ├── Member: Alice (role: member)        → gets benefits via seat
  └── Member: Bob (role: member)          → gets benefits via seat

Subscription (Team Pro — 3 seats)
  ├── CustomerSeat → Alice (claimed)      → benefits granted
  ├── CustomerSeat → Bob (claimed)        → benefits granted
  └── CustomerSeat → unassigned           → available
```

### Why this separation matters

This three-entity model gives you flexibility that a simple "buyer = user" model can't:

* **Members persist across products.** The same member can hold seats from different subscriptions or orders under the same customer.
* **Roles control portal access.** Owners and billing managers see full team management. Regular members see only their own benefits.
* **Benefits track to people, not purchases.** Always use `grant.member` to identify who has access — `grant.customer_id` is always the billing entity, not the end user.

### What this means for your integration

* **Benefit grants** reference a `member`, not just a `customer`. Always use `grant.member` to identify who received the benefit.
* **Webhooks** include a `member` object on seat and grant events. Use it instead of `customer_id` to identify the end user.
* **Benefits are not granted at purchase time.** Seats must be assigned and claimed before benefits are granted.

```
Purchase → Assign seats → Members claim → Benefits granted
```

## Prerequisites

* Polar organization with seat-based pricing enabled
* Polar SDK installed (`npm install @polar-sh/sdk` or `pip install polar-sdk`) — make sure it's the latest version
* Basic understanding of Polar products and subscriptions

## Implementation

<Info>
  This guide covers both **subscription** and **one-time purchase** seat-based products. The flow is the same — the only difference is in scaling and billing.
</Info>

### Step 1: Create a seat-based product

<Steps>
  <Step title="Navigate to Products">
    In the Polar dashboard, go to **Products** and click **Create Product**.
  </Step>

  <Step title="Configure pricing">
    Under **Pricing**:

    * **Product type**: Subscription (recurring) or One-time (perpetual licenses)
    * **Pricing type**: Seat-based
    * **Tiering model**: Choose how seats are priced (see below)

    **Fixed price per seat** (default) — every seat costs the same flat rate. Enter a single price per seat.

    **Graduated** — seats are priced per tier range independently. Each tier's seats are billed at that tier's rate and the totals are summed. Example with 1–10 seats at $10 and 11+ at $8: buying 14 seats = 10 × $10 + 4 × $8 = \$132.

    **Volume discounts** — the per-seat price is determined by the total seats purchased, and that rate applies to all seats. Example with the same tiers: buying 14 seats = 14 × $8 = $112 (all seats at the 11+ rate).

    | Tier | Max Seats | Price per Seat |
    | ---- | --------- | -------------- |
    | 1    | 4         | \$10/month     |
    | 2    | 9         | \$9/month      |
    | 3    | Unlimited | \$8/month      |
  </Step>

  <Step title="Add benefits">
    Configure benefits that seat holders will receive (license keys, file downloads, Discord roles, etc.). Remember: benefits are granted on seat claim, not at purchase.
  </Step>
</Steps>

### Step 2: Checkout

```typescript theme={null}
const checkout = await polar.checkouts.create({
  products: ["prod_123"],
  seats: 5,
  success_url: "https://your-app.com/success",
  customer_email: "billing@company.com"
});

// Redirect to checkout.url
```

The checkout automatically calculates pricing based on your tiers.

### Step 3: Seat management

After purchase, the billing manager can assign and manage seats directly from the **Customer Portal** — no custom UI required. The portal provides:

* **Assign seats** to team members by email
* **Revoke seats** to remove access and free up seats for reassignment
* **Resend invitations** for pending seats
* **Adjust seat count** on subscriptions (add or reduce)

Polar automatically sends invitation emails to assigned team members with a secure claim link. Once a member claims their seat, benefits are granted immediately.

<Info>
  Invitation emails are sent automatically when seats are assigned. The claim flow is fully managed by Polar — members click the link, claim their seat, and get immediate access to benefits.
</Info>

#### API-driven seat assignment

If you want to manage seat assignment programmatically (e.g., auto-assigning seats when users sign up), use the [Customer Seats API](/api-reference/customer-seats/assign):

```typescript theme={null}
const seat = await polar.customerSeats.assign({
  subscription_id: subscriptionId,
  email: "engineer@company.com",
  immediate_claim: true  // Skip invitation email, grant benefits immediately
});
```

<Info>
  Use `immediate_claim: true` when you manage your own user authentication and want to bypass the email invitation flow. Benefits are granted immediately without the member needing to click a claim link.
</Info>

### Step 4: Handle benefit grants

Listen for benefit webhooks to sync access in your system. Remember: use `grant.member` to identify the recipient, not `grant.customer_id` (which is the buyer).

<Warning>
  Always [verify webhook signatures](/integrate/webhooks/endpoints#verify-signature) before processing events. The example below omits verification for brevity.
</Warning>

```typescript theme={null}
app.post('/webhooks/polar', async (req, res) => {
  // Verify webhook signature first — see webhook docs
  const event = req.body;

  if (event.type === 'benefit_grant.created') {
    const grant = event.data;
    const recipient = grant.member;
    await grantAccess(recipient.id, recipient.email, grant.benefit);
  }

  if (event.type === 'benefit_grant.revoked') {
    const grant = event.data;
    await revokeAccess(grant.member.id, grant.benefit);
  }

  res.sendStatus(200);
});
```

### Step 5: Scale seats

**Subscriptions** — modify the seat count:

```typescript theme={null}
await polar.subscriptions.update({
  id: subscriptionId,
  seats: newTotal  // Cannot be less than currently assigned seats (pending + claimed)
});
```

<Warning>
  You cannot reduce seats below the number of currently assigned seats (both pending and claimed). Revoke seats first before reducing the count.
</Warning>

**One-time purchases** — buy a new order:

```typescript theme={null}
const checkout = await polar.checkouts.create({
  products: [productId],
  seats: additionalSeats,
  success_url: "https://your-app.com/success"
});
// Each order has its own independent seat pool
```

## Member sessions and portal

To give a member access to the customer portal, create a customer session with a `member_id` to scope the view:

```typescript theme={null}
const session = await polar.customerSessions.create({
  customer_id: "cust_123",
  member_id: "mem_789"
});
// Redirect to session.customer_portal_url
```

* **Billing managers** (owner/billing\_manager role) see full team management — assigning seats, managing members, and viewing seat utilization.
* **Members** see only their own benefits and account details.

## Webhook events

| Event                    | When                           | Key fields                             |
| ------------------------ | ------------------------------ | -------------------------------------- |
| `order.paid`             | Customer completes purchase    | `customer_id`, `product`               |
| `subscription.updated`   | Seat count changes             | `customer_id`, `seats`                 |
| `customer_seat.assigned` | Seat assigned, invitation sent | `member`, `email`, `status: "pending"` |
| `customer_seat.claimed`  | Member claims their seat       | `member`, `status: "claimed"`          |
| `benefit_grant.created`  | Benefit granted to member      | `member`, `customer_id` (buyer)        |
| `customer_seat.revoked`  | Seat revoked                   | `member`, `status: "revoked"`          |
| `benefit_grant.revoked`  | Benefit removed from member    | `member`, `customer_id` (buyer)        |

<Info>
  On subscription cancellation, each seat is revoked individually. A subscription with 5 seats and 3 benefits produces 1 + 5 + 15 = 21 webhook events. Make sure your handlers are idempotent.
</Info>

## Best practices

* **Use `grant.member` everywhere** — not `grant.customer_id` — to identify who has access
* **Use seat metadata** to store department, role, or cost center for your own tracking
* **Communicate clearly** to billing managers that they won't receive benefits automatically — they can assign a seat to themselves if they also want access

## Troubleshooting

| Problem             | Solution                                                                                              |
| ------------------- | ----------------------------------------------------------------------------------------------------- |
| Cannot reduce seats | Revoke assigned seats first. You can't go below the combined pending + claimed count.                 |
| Claim link expired  | Tokens expire after 24 hours. Resend via the portal or API: `polar.customerSeats.resend({ seat_id })` |

## Subscriptions vs one-time purchases

|                   | Subscriptions              | One-Time Purchases  |
| ----------------- | -------------------------- | ------------------- |
| **Payment**       | Recurring (monthly/yearly) | Single payment      |
| **Seat duration** | While subscribed           | Perpetual           |
| **Adding seats**  | Modify subscription        | Purchase new order  |
| **Benefits**      | While subscription active  | Forever after claim |

## Limitations

* Seats must be assigned individually (use API for bulk)
* Claim links expire after 24 hours
* Maximum 1,000 seats per subscription
* Metadata limited to 10 keys and 1KB per seat

## Next steps

* [Seat-Based Pricing Feature Documentation](/features/seat-based-pricing)
* [Customer Seats API Reference](/api-reference/customer-seats/assign)
* [Webhook setup](/integrate/webhooks/endpoints)
* [Customer Portal customization](/features/customer-portal)
