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

# Implementing Seat-Based Pricing

> Sell team plans where one payer buys seats and assigns them to teammates — each seat carries its own benefits.

Most billing assumes the person who pays is the person who uses the product. Team plans break that assumption: a finance lead buys ten seats, then ten engineers each get their own license key, Discord role, or download. Seat-based pricing models that split — billing on one side, entitlements on the other — and Macropay handles the seller-of-record paperwork (global sales tax and VAT, disputes, PCI) on top.

<Warning>
  Seat-based pricing is in **private beta**. Reach out to support to enable it for your organization before you build against it.
</Warning>

## The three entities

A seat-based product introduces a layer that single-buyer products don't have. Get these three concepts straight and every API call, webhook, and portal screen will make sense.

| Entity           | Answers                 | Notes                                                                                                                                                                        |
| ---------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Customer**     | Who pays?               | The billing entity. Owns subscriptions, orders, payment methods. On the first seat-based purchase it is permanently upgraded to `type: "team"`.                              |
| **Member**       | Who uses?               | A person under a customer, with their own email and role (`owner`, `billing_manager`, or `member`). Receives benefit grants independently. The purchaser becomes an `owner`. |
| **CustomerSeat** | Who's assigned to what? | The link between a product and a member. Tracks status (`pending`, `claimed`, `revoked`), holds the invitation token, and carries optional metadata.                         |

Roles map directly to capability. Both `owner` and `billing_manager` can assign seats, change or cancel the subscription, and manage payment methods. A plain `member` sees only their own benefits. (The `owner` role may gain extra management powers later.)

Here's how it fits together for a six-seat team plan, of which four seats are in use:

```
Customer: Maya (purchaser, type: "team")
  ├── Member: Maya   (owner)            → runs the team; can self-assign a seat for access
  ├── Member: Devon  (member)           → access via seat
  ├── Member: Priya  (member)           → access via seat
  └── Member: Sol    (member)           → access via seat

Subscription: "Studio Pro" — 6 seats
  ├── CustomerSeat → Devon  (claimed)   → benefits granted
  ├── CustomerSeat → Priya  (claimed)   → benefits granted
  ├── CustomerSeat → Sol    (claimed)   → benefits granted
  └── CustomerSeat → unassigned ×2      → available
```

### Why the split is worth it

* **Members outlive any single purchase.** One member can hold seats from several subscriptions or one-time orders under the same customer.
* **Entitlements attach to people, not invoices.** Benefits are granted when a seat is *claimed*, never at checkout.
* **The portal adapts to the role.** Owners and billing managers get team management; members get a personal benefits view.

<Warning>
  The most common integration bug: reading `grant.customer_id` to decide who gets access. That field is always the **payer**. The end user lives on `grant.member`. Use `grant.member` everywhere you provision access.
</Warning>

The lifecycle, in one line:

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

## What you'll need

* A Macropay organization with seat-based pricing enabled (private beta)
* The latest Macropay SDK — `npm install @macropayments/sdk` or `pip install macropay`
* Familiarity with Macropay [products](/features/products) and subscriptions

<Info>
  This guide covers both **subscription** and **one-time** seat products. The assignment and claim flow is identical; only how you add seats later differs (modify the subscription vs. buy another order).
</Info>

## Step 1 — Create the product

<Steps>
  <Step title="Open Products">
    In the [dashboard](https://app.macropay.ai), go to **Products** and click **Create Product**.
  </Step>

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

    * **Product type** — Subscription (recurring) or One-time (perpetual licenses)
    * **Pricing type** — Seat-based
    * **Min seats** — `1`, or your minimum team size

    Seat pricing is **volume-based**: the total seat count picks a tier, and that tier's rate applies to *every* seat. Buy 7 seats and you land in the 5–9 tier, so all 7 bill at that tier's rate (7 × $9 = $63/mo).

    | Tier | Up to | Per seat |
    | ---- | ----- | -------- |
    | 1    | 4     | \$10/mo  |
    | 2    | 9     | \$9/mo   |
    | 3    | ∞     | \$8/mo   |
  </Step>

  <Step title="Attach benefits">
    Add the benefits each seat holder receives — license keys, file downloads, Discord roles, GitHub repo access, and so on. These are granted on **claim**, not at purchase.
  </Step>
</Steps>

## Step 2 — Checkout

Pass `seats` and let Macropay compute the volume price from your tiers. Because Macropay is the **merchant of record**, the sales tax or VAT for the buyer's jurisdiction is calculated and remitted for you — it never lands on your tax filings.

```typescript theme={null}
const checkout = await macropay.checkouts.create({
  products: ["prod_studio_pro"],
  seats: 7,
  customer_email: "ops@northwind.example",
  success_url: "https://app.northwind.example/billing/done",
});

// Send the buyer to checkout.url
```

## Step 3 — Assign seats

Once the order is paid, an owner or billing manager can run seat management straight from the **[Customer Portal](/features/customer-portal)** — no UI to build. The portal lets them:

* **Assign** a seat to a teammate by email
* **Revoke** a seat to cut access and free it for reuse
* **Resend** the invite for a still-pending seat
* **Adjust** the seat count on a subscription

Assigning a seat fires an invitation email with a secure claim link. The moment the teammate claims it, their benefits are granted.

<Info>
  The invite and claim flow is fully hosted by Macropay. Members click the link, claim the seat, and get immediate access — you don't render or send anything.
</Info>

### Assigning seats from your own backend

If you already authenticate users and want to wire seats to signups, call the [Customer Seats API](/api-reference/customer-seats/assign) directly:

```typescript theme={null}
const seat = await macropay.customerSeats.assign({
  subscription_id: subscriptionId,
  email: "new.hire@northwind.example",
  immediate_claim: true, // skip the email, grant benefits now
});
```

<Tip>
  Set `immediate_claim: true` when your app owns identity. It bypasses the invitation email and grants benefits on the spot — ideal for auto-provisioning a seat the instant a user joins a workspace you already control.
</Tip>

## Step 4 — Provision on benefit grants

Listen for benefit-grant webhooks and mirror access in your own system. The recipient is `grant.member`; `grant.customer_id` is the payer.

<Warning>
  Always [verify the webhook signature](/integrate/webhooks/endpoints#verify-signature) before trusting an event. The handler below omits verification for brevity only.
</Warning>

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

  switch (event.type) {
    case "benefit_grant.created": {
      const grant = event.data;
      await grantAccess(grant.member.id, grant.member.email, grant.benefit);
      break;
    }
    case "benefit_grant.revoked": {
      const grant = event.data;
      await revokeAccess(grant.member.id, grant.benefit);
      break;
    }
  }

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

## Step 5 — Change the seat count

**Subscriptions** — patch the seat total:

```typescript theme={null}
await macropay.subscriptions.update({
  id: subscriptionId,
  seats: newTotal, // must be ≥ currently assigned seats (pending + claimed)
});
```

<Warning>
  You can't drop below the number of assigned seats (pending **plus** claimed). Revoke the seats you no longer need first, then reduce the count.
</Warning>

**One-time purchases** — buy another order; each has its own independent seat pool:

```typescript theme={null}
const checkout = await macropay.checkouts.create({
  products: [productId],
  seats: additionalSeats,
  success_url: "https://app.northwind.example/billing/done",
});
```

## Giving members the portal

To drop a member into the customer portal — to see their benefits or, for managers, to run the team — create a member session and redirect to the returned URL:

```typescript theme={null}
const session = await macropay.memberSessions.create({
  member_id: "mem_4f2a",
});
// Redirect to session.member_portal_url
```

What they see depends on their role:

* **Owners and billing managers** — full team management: assign seats, manage members, review utilization.
* **Members** — their own benefits and account details, nothing more.

## Webhook reference

| Event                    | Fires when                    | Key fields                             |
| ------------------------ | ----------------------------- | -------------------------------------- |
| `order.paid`             | The purchase completes        | `customer_id`, `product`               |
| `subscription.updated`   | Seat count changes            | `customer_id`, `seats`                 |
| `customer_seat.assigned` | Seat assigned, invite sent    | `member`, `email`, `status: "pending"` |
| `customer_seat.claimed`  | Member claims the seat        | `member`, `status: "claimed"`          |
| `customer_seat.revoked`  | Seat revoked                  | `member`, `status: "revoked"`          |
| `benefit_grant.created`  | Benefit granted to a member   | `member`, `customer_id` (payer)        |
| `benefit_grant.revoked`  | Benefit removed from a member | `member`, `customer_id` (payer)        |

<Warning>
  Cancellations fan out. Each seat is revoked individually, so a 5-seat subscription carrying 3 benefits emits 1 + 5 + 15 = 21 events. Make every handler **idempotent**.
</Warning>

## Patterns worth adopting

* **Provision off `grant.member`, never `grant.customer_id`.** It's the single most reliable way to attribute access to the right person.
* **Use seat metadata for your own bookkeeping** — stash a department, cost center, or internal user ID on the seat (up to 10 keys / 1KB).
* **Tell billing managers they aren't auto-enrolled.** A manager who also wants the product must assign a seat to themselves.

## Troubleshooting

| Symptom                     | Fix                                                                                                       |
| --------------------------- | --------------------------------------------------------------------------------------------------------- |
| Can't reduce the seat count | Revoke assigned seats first; you can't go below pending + claimed.                                        |
| Claim link no longer works  | Tokens expire after 24 hours. Resend from the portal or via `macropay.customerSeats.resend({ seat_id })`. |
| Wrong user provisioned      | You read `customer_id`. Switch to `grant.member`.                                                         |

## Subscriptions vs. one-time

|                   | Subscription                     | One-time            |
| ----------------- | -------------------------------- | ------------------- |
| **Payment**       | Recurring (monthly / yearly)     | Single charge       |
| **Seat duration** | While subscribed                 | Perpetual           |
| **Adding seats**  | Modify the subscription          | Buy a new order     |
| **Benefits**      | While the subscription is active | Forever after claim |

## Limits

* Seats are assigned one at a time (script the API for bulk).
* Claim links expire after 24 hours.
* Up to 1,000 seats per subscription.
* Seat metadata: 10 keys, 1KB max.

## Where to go next

* [Seat-based pricing feature overview](/features/seat-based-pricing)
* [Customer Seats API](/api-reference/customer-seats/assign)
* [Setting up webhooks](/integrate/webhooks/endpoints)
* [Customizing the customer portal](/features/customer-portal)
