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

## 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 Crosspoint 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. (Exception: when using the default Crosspoint confirmation page, the buyer's seat is auto-claimed.)

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

## Prerequisites

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

## Implementation

> [!NOTE]
>
> 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.

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

**1. Navigate to Products**

Open the dashboard and [create a new product](https://app.crosspoint.finance/to/dashboard/products/new).

**2. 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 |

**3. 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 2: Checkout

```typescript theme={null}
const checkout = await crosspoint.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)

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

> [!NOTE]
>
> Invitation emails are sent automatically when seats are assigned. The claim flow is fully managed by Crosspoint — members click the link, claim their seat, and get immediate access to benefits.

#### 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](https://staging-docs.crosspoint.finance/api-reference/current/customer-seats/assign-seat.md):

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

> [!NOTE]
>
> 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.

### 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](https://staging-docs.crosspoint.finance/integrate/webhooks/endpoints.md#verify-signature) before processing events. The example below omits verification for brevity.

```typescript theme={null}
app.post('/webhooks/crosspoint', 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 crosspoint.subscriptions.update(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.

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

```typescript theme={null}
const checkout = await crosspoint.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 crosspoint.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) |

> [!NOTE]
>
> 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.

## 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 about seat assignment: if using the default Crosspoint confirmation page, their seat is auto-claimed; if using a custom success URL, they'll need to assign themselves a seat through the portal

## 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: `crosspoint.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](https://staging-docs.crosspoint.finance/features/seat-based-pricing.md)
* [Customer Seats API Reference](https://staging-docs.crosspoint.finance/api-reference/current/customer-seats/assign-seat.md)
* [Webhook setup](https://staging-docs.crosspoint.finance/integrate/webhooks/endpoints.md)
* [Customer Portal customization](https://staging-docs.crosspoint.finance/features/customer-portal.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.