> ## Documentation Index
> Fetch the complete documentation index at: https://docs.emby.ai/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

This guide walks you through implementing seat-based pricing for team products, from creating the product to handling seat assignments and claims.

## What you'll build

By the end of this guide, you'll have:

* A seat-based product with tiered pricing (subscription or one-time)
* Checkout flow for purchasing seats
* Seat assignment and management interface
* Claim flow for team members

<Info>
  This guide covers both **subscription-based** and **one-time purchase** seat-based products. The implementation is similar for both, with key differences in scaling and billing.
</Info>

## Prerequisites

* Polar organization with `seat_based_pricing_enabled` feature flag
* Polar SDK installed (`npm install @polar-sh/sdk` or `pip install polar-sdk`)
  * **Important:** Seat-based pricing requires the latest version of the SDK. Make sure to update to the latest version to access all seat-based pricing features.
* Basic understanding of Polar products and subscriptions

<Warning>
  Seat-based pricing is currently in **private beta**. To enable the `seat_based_pricing_enabled` feature flag for your organization, please contact our support team.
</Warning>

## 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 basic settings">
    Set your product name, description, and media. For example:

    * **Subscription**: Team Pro Plan - Professional features for your entire team
    * **One-time**: Enterprise License Pack - Perpetual team licenses
  </Step>

  <Step title="Select seat-based pricing">
    Under **Pricing**:

    * **Product type**: Choose **Subscription** for recurring billing or **One-time** for perpetual licenses
    * **Billing cycle** (subscriptions only): Monthly or Yearly
    * **Pricing type**: Seat-based
    * **Min seats**: 1 (or your minimum team size)
  </Step>

  <Step title="Configure pricing tiers">
    Define your volume-based pricing:

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

    Example: A team purchasing 6 seats pays 6 × $9 = $54/month.
  </Step>

  <Step title="Add benefits">
    Configure benefits that seat holders will receive:

    * License Keys
    * File Downloads
    * Discord roles
    * Custom benefits

    <Info>
      Benefits are granted when seats are claimed, not at purchase time.
    </Info>
  </Step>
</Steps>

You can also create seat-based products via API:

**Subscription example:**

```typescript theme={null}
const subscriptionProduct = await polar.products.create({
  name: "Team Pro Plan",
  organization_id: "org_123",
  is_recurring: true,
  prices: [{
    type: "recurring",
    recurring_interval: "month",
    amount_type: "seat_based",
    price_currency: "usd",
    seat_tiers: [
      { min_seats: 1, max_seats: 4, price_per_seat: 1000 },   // $10/month
      { min_seats: 5, max_seats: 9, price_per_seat: 900 },    // $9/month
      { min_seats: 10, max_seats: null, price_per_seat: 800 } // $8/month
    ]
  }]
});
```

**One-time purchase example:**

```typescript theme={null}
const oneTimeProduct = await polar.products.create({
  name: "Enterprise License Pack",
  organization_id: "org_123",
  is_recurring: false,
  prices: [{
    type: "one_time",
    amount_type: "seat_based",
    price_currency: "usd",
    seat_tiers: [
      { min_seats: 1, max_seats: 10, price_per_seat: 5000 },   // $50 per seat
      { min_seats: 11, max_seats: 50, price_per_seat: 4500 },  // $45 per seat
      { min_seats: 51, max_seats: null, price_per_seat: 4000 } // $40 per seat
    ]
  }]
});
```

## Step 2: Implement checkout flow

Create a checkout session that allows customers to select seat quantity:

```typescript theme={null}
const checkout = await polar.checkouts.create({
  product_price_id: "price_123",
  seats: 5, // Customer selects quantity
  success_url: "https://yourapp.com/success",
  customer_email: "billing@company.com"
});

// Redirect to checkout.url
```

The checkout displays:

* Price per seat based on quantity
* Total amount
* Clear indication this is for team access

<Info>
  The checkout automatically calculates pricing based on your tiers. A customer selecting 5 seats will see the $9/seat price, totaling $45.
</Info>

## Step 3: Handle post-purchase webhook

Listen for purchase webhooks to know when a customer buys seats:

```typescript theme={null}
// Webhook handler
app.post('/webhooks/polar', async (req, res) => {
  const event = req.body;

  // For subscriptions
  if (event.type === 'subscription.created') {
    const subscription = event.data;

    if (subscription.product.has_seat_based_price) {
      await notifyBillingManager(subscription.customer_id, {
        message: `Your ${subscription.seats}-seat subscription is active!`,
        manage_seats_url: `https://yourapp.com/seats/subscription/${subscription.id}`
      });
    }
  }

  // For one-time purchases
  if (event.type === 'order.created') {
    const order = event.data;

    if (order.seats) {
      await notifyBillingManager(order.customer_id, {
        message: `Your ${order.seats} perpetual seat licenses have been purchased!`,
        manage_seats_url: `https://yourapp.com/seats/order/${order.id}`
      });
    }
  }

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

## Step 4: Build seat management interface

Create an interface for billing managers to assign seats:

```typescript theme={null}
// List available seats (works for both subscriptions and orders)
async function getSeatInfo(params: { subscription_id?: string; order_id?: string }) {
  const { seats, available_seats, total_seats } =
    await polar.customerSeats.list(params);

  return {
    seats,
    available: available_seats,
    total: total_seats,
    canAssign: available_seats > 0
  };
}

// Assign a seat (works for both subscriptions and orders)
async function assignSeat(
  params: { subscription_id?: string; order_id?: string },
  email: string,
  metadata?: Record<string, any>
) {
  try {
    const seat = await polar.customerSeats.assign({
      ...params,
      email: email,
      metadata: metadata // e.g., { department: "Engineering" }
    });

    return {
      success: true,
      seat: seat,
      message: `Invitation sent to ${email}`
    };
  } catch (error) {
    if (error.status === 400) {
      return {
        success: false,
        error: "No seats available or customer already has a seat"
      };
    }
    throw error;
  }
}

// Example usage:
// For subscriptions: getSeatInfo({ subscription_id: "sub_123" })
// For orders: getSeatInfo({ order_id: "order_456" })
```

Example UI component (React):

```tsx theme={null}
function SeatManagement({ subscriptionId }: { subscriptionId: string }) {
  const [seatInfo, setSeatInfo] = useState(null);
  const [email, setEmail] = useState("");

  useEffect(() => {
    loadSeats();
  }, [subscriptionId]);

  async function loadSeats() {
    const info = await getSeatInfo(subscriptionId);
    setSeatInfo(info);
  }

  async function handleAssign() {
    const result = await assignSeat(subscriptionId, email, {
      role: "Developer"
    });

    if (result.success) {
      setEmail("");
      loadSeats(); // Refresh list
      toast.success("Invitation sent!");
    }
  }

  return (
    <div>
      <h2>Seat Management</h2>
      <p>{seatInfo?.available} of {seatInfo?.total} seats available</p>

      {/* Assign new seat */}
      <div>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="team-member@company.com"
        />
        <button
          onClick={handleAssign}
          disabled={!seatInfo?.canAssign}
        >
          Assign Seat
        </button>
      </div>

      {/* List existing seats */}
      <table>
        <thead>
          <tr>
            <th>Email</th>
            <th>Status</th>
            <th>Role</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {seatInfo?.seats.map(seat => (
            <tr key={seat.id}>
              <td>{seat.customer_email}</td>
              <td>
                <SeatStatusBadge status={seat.status} />
              </td>
              <td>{seat.seat_metadata?.role}</td>
              <td>
                {seat.status === 'pending' && (
                  <button onClick={() => resendInvitation(seat.id)}>
                    Resend
                  </button>
                )}
                {seat.status === 'claimed' && (
                  <button onClick={() => revokeSeat(seat.id)}>
                    Revoke
                  </button>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

## Step 5: Implement seat claim flow

When a team member receives an invitation email, they'll click a link with the invitation token. Build a claim page:

```typescript theme={null}
// Claim page route: /claim?token=abc123...

async function handleClaimPage(token: string) {
  // Get claim information (no auth required)
  const claimInfo = await polar.customerSeats.getClaimInfo({
    invitation_token: token
  });

  if (!claimInfo.can_claim) {
    return {
      error: "This invitation has expired or already been claimed"
    };
  }

  return {
    product: claimInfo.product_name,
    organization: claimInfo.organization_name,
    email: claimInfo.customer_email
  };
}

async function claimSeat(token: string) {
  const { seat, customer_session_token } =
    await polar.customerSeats.claim({
      invitation_token: token
    });

  // Store the customer session token
  // This allows immediate portal access
  localStorage.setItem('polar_session', customer_session_token);

  return {
    success: true,
    seat: seat,
    sessionToken: customer_session_token
  };
}
```

Example claim page (React):

```tsx theme={null}
function ClaimPage() {
  const [token] = useSearchParams();
  const [claimInfo, setClaimInfo] = useState(null);
  const [claiming, setClaiming] = useState(false);

  useEffect(() => {
    loadClaimInfo();
  }, [token]);

  async function loadClaimInfo() {
    const info = await handleClaimPage(token.get('token'));
    setClaimInfo(info);
  }

  async function handleClaim() {
    setClaiming(true);
    try {
      const result = await claimSeat(token.get('token'));

      // Redirect to customer portal
      window.location.href = `/portal?session=${result.sessionToken}`;
    } catch (error) {
      toast.error("Failed to claim seat");
      setClaiming(false);
    }
  }

  if (claimInfo?.error) {
    return <div>Error: {claimInfo.error}</div>;
  }

  return (
    <div>
      <h1>You've been invited!</h1>
      <p>
        Join {claimInfo?.organization}'s {claimInfo?.product} plan
      </p>
      <p>Email: {claimInfo?.email}</p>

      <button onClick={handleClaim} disabled={claiming}>
        {claiming ? "Claiming..." : "Claim My Seat"}
      </button>
    </div>
  );
}
```

## Step 6: Handle benefit granting

After a seat is claimed, benefits are granted automatically via background jobs. Listen for webhooks to track this:

```typescript theme={null}
app.post('/webhooks/polar', async (req, res) => {
  const event = req.body;

  if (event.type === 'benefit_grant.created') {
    const grant = event.data;

    // A team member received their benefits
    console.log(`Benefit ${grant.benefit_id} granted to ${grant.customer_id}`);

    // Update your app (e.g., create license, grant access)
    await grantAccess(grant.customer_id, grant.benefit);
  }

  if (event.type === 'benefit_grant.revoked') {
    const grant = event.data;

    // A seat was revoked
    await revokeAccess(grant.customer_id, grant.benefit);
  }

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

## Step 7: Implement seat revocation

Allow billing managers to revoke seats:

```typescript theme={null}
async function revokeSeat(seatId: string) {
  const revokedSeat = await polar.customerSeats.revoke({
    seat_id: seatId
  });

  // Benefits are automatically revoked via webhook
  return {
    success: true,
    seat: revokedSeat,
    message: "Seat revoked successfully"
  };
}
```

<Warning>
  Revoking a seat immediately removes access but does not issue a refund. The billing manager continues to pay for all purchased seats.
</Warning>

## Step 8: Handle scaling

**For subscriptions**, allow billing managers to add or reduce seats:

```typescript theme={null}
async function addSeats(subscriptionId: string, newTotal: number) {
  // Update subscription seat count
  const subscription = await polar.subscriptions.update({
    id: subscriptionId,
    seats: newTotal
  });

  // New seats are immediately available for assignment
  return subscription;
}

async function reduceSeats(subscriptionId: string, newTotal: number) {
  const { seats } = await polar.customerSeats.list({
    subscription_id: subscriptionId
  });

  const claimedCount = seats.filter(s => s.status === 'claimed').length;

  if (newTotal < claimedCount) {
    throw new Error(
      `Cannot reduce to ${newTotal} seats. ${claimedCount} seats are currently claimed. Revoke seats first.`
    );
  }

  // Update will take effect at next renewal
  const subscription = await polar.subscriptions.update({
    id: subscriptionId,
    seats: newTotal
  });

  return subscription;
}
```

**For one-time purchases**, customers buy additional seats via new orders:

```typescript theme={null}
async function purchaseMoreSeats(productId: string, additionalSeats: number) {
  // Create a new checkout for additional seats
  const checkout = await polar.checkouts.create({
    product_id: productId,
    seats: additionalSeats,
    success_url: "https://yourapp.com/success"
  });

  // Each order is independent with its own seat pool
  return checkout;
}
```

<Info>
  For one-time purchases, each order has its own independent seat pool. Customers can purchase additional seats anytime by creating a new order. All seats remain perpetual.
</Info>

## Best Practices

### 1. Validate seat availability

Always check available seats before showing the assignment form:

```typescript theme={null}
if (available_seats === 0) {
  return (
    <div>
      All seats are assigned.
      <button onClick={upgradeSubscription}>
        Add More Seats
      </button>
    </div>
  );
}
```

### 2. Use metadata effectively

Store useful context in seat metadata:

```typescript theme={null}
await polar.customerSeats.assign({
  subscription_id: subId,
  email: "dev@company.com",
  metadata: {
    department: "Engineering",
    role: "Senior Developer",
    cost_center: "R&D",
    manager: "jane@company.com"
  }
});
```

### 3. Handle expired tokens gracefully

```typescript theme={null}
try {
  await claimSeat(token);
} catch (error) {
  if (error.status === 400) {
    // Show resend option
    return "This invitation has expired. Contact your admin to resend.";
  }
}
```

### 4. Track utilization

Monitor seat usage to identify upsell opportunities:

```typescript theme={null}
const { seats, available_seats, total_seats } = await getSeatInfo(subId);
const utilization = ((total_seats - available_seats) / total_seats) * 100;

if (utilization > 80) {
  // Suggest adding more seats
  showUpgradePrompt();
}
```

### 5. Clear communication

Make it clear to billing managers that:

* They won't receive direct access to benefits
* Seats must be assigned to team members
* Revocation doesn't refund costs
* **For subscriptions**: Reducing seats requires revoking claims first
* **For one-time purchases**: Seats are perpetual and non-refundable

## Common Patterns

### Bulk seat assignment

```typescript theme={null}
async function assignMultipleSeats(
  subscriptionId: string,
  emails: string[]
) {
  const results = await Promise.allSettled(
    emails.map(email =>
      polar.customerSeats.assign({
        subscription_id: subscriptionId,
        email: email
      })
    )
  );

  const succeeded = results.filter(r => r.status === 'fulfilled');
  const failed = results.filter(r => r.status === 'rejected');

  return {
    succeeded: succeeded.length,
    failed: failed.length,
    errors: failed.map(f => f.reason)
  };
}
```

### Syncing with your user system

```typescript theme={null}
// When a user joins your system
async function onUserSignup(email: string, organizationId: string) {
  // Check if they have a pending seat
  const subscriptions = await getOrganizationSubscriptions(organizationId);

  for (const sub of subscriptions) {
    const { seats } = await polar.customerSeats.list({
      subscription_id: sub.id
    });

    const pendingSeat = seats.find(s =>
      s.status === 'pending' && s.customer_email === email
    );

    if (pendingSeat) {
      // Auto-claim on signup
      const claimLink = `/claim?token=${pendingSeat.invitation_token}`;
      return { shouldClaim: true, claimLink };
    }
  }
}
```

### Custom portal integration

```typescript theme={null}
// Display team subscriptions in your app
async function getTeamSubscriptions(customerId: string) {
  const subs = await polar.customerPortal.seats.listSubscriptions({
    customer_id: customerId
  });

  return subs.map(sub => ({
    product: sub.product.name,
    status: sub.status,
    role: sub.seat_metadata?.role,
    expires: sub.current_period_end
  }));
}
```

## Troubleshooting

### Seats not appearing

Ensure the feature flag is enabled:

```
seat_based_pricing_enabled: true
```

### Benefits not granted after claim

Check webhook logs for `benefit_grant.created` events. Benefits are granted asynchronously via background jobs.

### Cannot reduce seats

Make sure to revoke seats before reducing the subscription seat count. You cannot reduce below the currently claimed count.

### Claim link expired

Invitation tokens expire after 24 hours. Have the billing manager resend the invitation:

```typescript theme={null}
await polar.customerSeats.resend({ seat_id: seatId });
```

## Next Steps

* Review the [Seat-Based Pricing Feature Documentation](/features/seat-based-pricing)
* Explore the [Customer Seats API Reference](/api-reference/customer-seats/assign)
* Set up [webhook handlers](/integrate/webhooks/endpoints) for real-time updates
* Learn about [Customer Portal customization](/features/customer-portal)

## Need Help?

Join our [Discord community](https://polar.sh/discord) or contact support for assistance with seat-based pricing implementation.
