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

# Move Existing Seats to Members

> What shifts when your organization adopts the member model, and how to adapt your integration

export const RubaSocials = () => {
  if (typeof document === "undefined") {
    return null;
  }
  const inject = () => {
    const footer = document.getElementById("footer");
    if (!footer || document.getElementById("ruba-socials")) {
      return false;
    }
    const isDark = document.querySelector("html.dark") || document.querySelector('html[class*="dark"]');
    const lineColor = isDark ? "#383838" : "#e8e8ed";
    const iconColor = isDark ? "#8e8e93" : "#86868b";
    const markColor = isDark ? "#ffffff" : "#08080c";
    const hoverColor = "#0071e3";
    const div = document.createElement("div");
    div.id = "ruba-socials";
    div.style.cssText = `display:flex;align-items:center;gap:20px;padding-top:20px;margin-top:20px;border-top:1px solid ${lineColor};width:100%`;
    const logo = `<a href="https://getruba.com" style="display:inline-flex;height:24px;align-items:center;gap:8px;text-decoration:none;color:${markColor};line-height:1;"><svg width="24" height="24" viewBox="0 0 120 120" aria-hidden="true" style="display:block;flex:none;"><g transform="translate(2.5 0)"><path d="M19 25h59L65.43 47H19a4 4 0 0 1-4-4V29a4 4 0 0 1 4-4Z" fill="${markColor}"/><path d="M19 73h31.57L38 95H19a4 4 0 0 1-4-4V77a4 4 0 0 1 4-4Z" fill="${markColor}"/><path d="M85 25h25L70 95H45Z" fill="#007AFF"/></g></svg><span style="display:inline-flex;height:24px;align-items:center;font-weight:600;font-size:15px;color:${markColor};letter-spacing:-0.01em;line-height:1;">Ruba</span></a>`;
    const spacer = `<div style="flex:1"></div>`;
    const ig = `<a href="https://instagram.com/getruba" target="_blank" rel="noopener" aria-label="Instagram" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/></svg></a>`;
    const li = `<a href="https://linkedin.com/company/getruba" target="_blank" rel="noopener" aria-label="LinkedIn" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg></a>`;
    const x = `<a href="https://x.com/getruba" target="_blank" rel="noopener" aria-label="X" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg></a>`;
    const github = `<a href="https://github.com/rubadot" target="_blank" rel="noopener" aria-label="GitHub" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .7a11.5 11.5 0 0 0-3.64 22.4c.58.1.79-.25.79-.56v-2.24c-3.22.7-3.9-1.37-3.9-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.71.08-.71 1.16.08 1.78 1.2 1.78 1.2 1.04 1.77 2.72 1.26 3.38.96.1-.75.4-1.26.74-1.55-2.57-.3-5.27-1.29-5.27-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.47.11-3.05 0 0 .97-.31 3.16 1.18A10.96 10.96 0 0 1 12 6.09c.98 0 1.94.13 2.86.39 2.2-1.49 3.16-1.18 3.16-1.18.63 1.58.23 2.76.11 3.05.74.8 1.19 1.83 1.19 3.09 0 4.42-2.7 5.39-5.28 5.68.42.36.79 1.06.79 2.14v3.28c0 .31.21.67.8.56A11.5 11.5 0 0 0 12 .7Z"/></svg></a>`;
    div.innerHTML = logo + spacer + ig + li + x + github;
    footer.appendChild(div);
    return true;
  };
  if (!document.documentElement.dataset.rubaSocialsObserver) {
    document.documentElement.dataset.rubaSocialsObserver = "true";
    const observer = new MutationObserver(() => {
      if (!document.getElementById("ruba-socials")) {
        inject();
      }
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }
  setTimeout(() => {
    if (!inject()) {
      const interval = setInterval(() => {
        if (inject()) {
          clearInterval(interval);
        }
      }, 500);
      setTimeout(() => clearInterval(interval), 10000);
    }
  }, 300);
  return null;
};

<link rel="stylesheet" href="/style.css" />

<RubaSocials />

If you sell seat-based products on Ruba, your organization will move over to the **member model**. This guide spells out what shifts, what breaks, and exactly how to adapt your code.

## What the member model brings

The member model introduces a split between **who pays** and **who uses** through three entities:

* A **Customer** is the billing entity (who pays). They own subscriptions, orders, and payment methods. After migration, customers with seat-based products are upgraded to `type: "team"`.
* A **Member** is a person under a customer (who uses). Each member has their own email and role (`owner`, `billing_manager`, or `member`), and receives benefit grants independently.
* A **CustomerSeat** is the link between a subscription and a member. It tracks assignment status and carries metadata.

Today, each seat holder is represented as their own `Customer`. After migration, seat holders become `Member` records under the purchasing customer, and `CustomerSeat` links the subscription to each member.

For a complete overview of how these entities fit together, see the [Seat-Based Pricing guide](/guides/seat-based-pricing).

## How the migration plays out: two phases

The migration plays out in **two phases** so you can adapt your integration before the breaking change lands.

### Phase 1: Preparation (non-breaking)

In this phase, Ruba creates `Member` records for all your existing seat holders and populates `member_id` on seats and benefit grants. **Nothing breaks** because `customer_id` on seats still points to the seat holder, and no customers are deleted.

This buys you time to start reading the new `member` and `member_id` fields and adapt your code before the breaking change.

### Phase 2: Member model enabled (breaking)

In this phase, the member model is fully switched on. `customer_id` on seats and grants **flips** to point to the billing customer (the buyer), old seat-holder customer records are deleted, and all new operations lean on the member model.

**This is the breaking change.** If your code reads `customer_id` to identify seat holders, it will now get the buyer instead.

## Breaking changes at a glance

These shifts happen in **Phase 2** only:

| What changed                 | Phase 1                   | Phase 2   | Fix                               |
| ---------------------------- | ------------------------- | --------- | --------------------------------- |
| `seat.customer_id`           | Still the seat holder     | The buyer | Use `seat.member` or `seat.email` |
| `grant.customer_id`          | Still the grant recipient | The buyer | Use `grant.member`                |
| Old seat-holder customer IDs | Still valid               | `404`     | Map to `member_id` via seat list  |

## How to adapt your code

<Info>
  Adapt your code during Phase 1 so everything works when Phase 2 switches on. The `member` field is available in both phases.
</Info>

### 1. Seats: identify holders by member, not customer\_id

Calling `GET /v1/customer-seats/{seat_id}` or listing seats for a subscription returns:

**Phase 1** (non-breaking, `customer_id` still points to Alice):

```json theme={"system"}
{
  "id": "seat_1",
  "subscription_id": "sub_456",
  "customer_id": "cust_alice",
  "member": {
    "id": "mem_alice",
    "email": "alice@company.com",
    "customer_id": "cust_jane",
    "role": "member"
  },
  "email": "alice@company.com",
  "customer_email": "alice@company.com"
}
```

**Phase 2** (breaking, `customer_id` flipped to Jane):

```json theme={"system"}
{
  "id": "seat_1",
  "subscription_id": "sub_456",
  "customer_id": "cust_jane",
  "member": {
    "id": "mem_alice",
    "email": "alice@company.com",
    "customer_id": "cust_jane",
    "role": "member"
  },
  "email": "alice@company.com",
  "customer_email": "alice@company.com"
}
```

* Use `seat.member` or `seat.email` to identify who holds the seat. This works in both phases.
* `seat.customer_email` still resolves to the holder's email, so it is safe to keep using.

### 2. Benefit grants: use grant.member for the recipient

Calling `GET /v1/benefit-grants/{grant_id}` or receiving a `benefit_grant.created` webhook returns:

**Phase 1:**

```json theme={"system"}
{
  "id": "grant_1",
  "customer_id": "cust_alice",
  "member": {
    "id": "mem_alice",
    "email": "alice@company.com",
    "customer_id": "cust_jane",
    "role": "member"
  },
  "benefit_id": "ben_789"
}
```

**Phase 2:**

```json theme={"system"}
{
  "id": "grant_1",
  "customer_id": "cust_jane",
  "member": {
    "id": "mem_alice",
    "email": "alice@company.com",
    "customer_id": "cust_jane",
    "role": "member"
  },
  "benefit_id": "ben_789"
}
```

* Use `grant.member` to know who received the benefit. This is consistent across both phases.
* License keys and downloads tied to this grant also transfer in Phase 2.

### 3. Replace stored seat-holder customer IDs

If you stored `cust_alice` or `cust_bob` in your database to track who has access, those IDs return `404` after Phase 2. During Phase 1, use the seat list endpoint to map old references to new member IDs:

```typescript theme={"system"}
// List all seats for a subscription to get the new member IDs
const seats = await ruba.customerSeats.list({
  subscription_id: "sub_456"
});

for (const seat of seats.result.items) {
  if (seat.member) {
    // Map: seat.member.email → seat.member.id
    await updateYourDatabase(seat.member.email, seat.member.id);
  }
}
```

### 4. Update webhook handlers

**Seat webhooks:**

```typescript theme={"system"}
// Before
if (event.type === 'customer_seat.claimed') {
  const holderId = event.data.customer_id; // ❌ Will be the buyer after Phase 2
  await grantAccess(holderId);
}

// After (works in both phases)
if (event.type === 'customer_seat.claimed') {
  const holder = event.data.member;        // ✅ Always the seat occupant
  await grantAccess(holder.id, holder.email);
}
```

**Benefit grant webhooks:**

```typescript theme={"system"}
if (event.type === 'benefit_grant.created') {
  const grant = event.data;

  if (grant.member) {
    // Seat-based grant: member is the end user
    await grantAccess(grant.member.id, grant.member.email);
  } else {
    // Direct purchase: customer is the end user
    await grantAccess(grant.customer_id);
  }
}
```

**New member lifecycle events** are now available if you want to track when team members join or leave:

* `member.created` is emitted when a member joins the team (via seat assignment or customer creation)
* `member.updated` is emitted when a member's details change
* `member.deleted` is emitted when a member is removed

### 5. Seat assignment

Existing assignment calls using `email`, `customer_id`, or `external_customer_id` keep working. Ruba resolves them to a member internally. However, the **response shifts** between phases:

**Phase 1:** `seat.customer_id` in the response still points to the seat holder.

```typescript theme={"system"}
const seat = await ruba.customerSeats.assign({
  subscription_id: "sub_456",
  email: "charlie@company.com"
});

seat.customer_id;    // "cust_charlie" (the seat holder)
seat.member.email;   // "charlie@company.com"
seat.member.id;      // "mem_charlie"
```

**Phase 2:** `seat.customer_id` in the response points to the buyer, not the seat holder.

```typescript theme={"system"}
const seat = await ruba.customerSeats.assign({
  subscription_id: "sub_456",
  email: "charlie@company.com"
});

seat.customer_id;    // "cust_jane" (the buyer, NOT charlie)
seat.member.email;   // "charlie@company.com"
seat.member.id;      // "mem_charlie"
```

If your code reads `customer_id` from the assignment response, switch to `seat.member` instead.

You can also assign by member ID directly:

```typescript theme={"system"}
const seat = await ruba.customerSeats.assign({
  subscription_id: "sub_456",
  member_id: "mem_charlie"
});
```

<Warning>
  Do not mix legacy identifiers (`customer_id`, `external_customer_id`) with member identifiers (`member_id`, `external_member_id`) in the same request.
</Warning>

### 6. Pass member\_id when creating customer sessions

After migration, pass a `member_id` when creating customer sessions to scope the portal view to a specific member:

```typescript theme={"system"}
// Before: customer session with no member context
const session = await ruba.customerSessions.create({
  customer_id: "cust_jane"
});

// After: pass member_id to scope the session
const session = await ruba.customerSessions.create({
  customer_id: "cust_jane",
  member_id: "mem_alice"
});
// Redirect to session.customer_portal_url
```

You can also use `external_member_id` as an alternative to `member_id`:

```typescript theme={"system"}
const session = await ruba.customerSessions.create({
  customer_id: "cust_jane",
  external_member_id: "your_user_id_alice"
});
```

* **Owners and billing managers** see full team management, including assigning seats, managing members, and adjusting seat count.
* **Regular members** see only their own benefits and account details.

<Info>
  For team customers, `member_id` is required. For individual customers, it is optional. When omitted, the owner member is used automatically.
</Info>

## Migration checklist

**During Phase 1 (do this now):**

* [ ] Adapt seat-handling code to use `seat.member` / `seat.email` instead of `seat.customer_id`
* [ ] Adapt benefit grant code to use `grant.member` instead of `grant.customer_id`
* [ ] Replace any stored seat-holder customer IDs with member IDs
* [ ] Update webhook handlers to read `member` from seat and grant payloads
* [ ] Pass `member_id` when calling `customerSessions.create()` for portal access

**Optional enhancements:**

* [ ] Use `member_id` or `external_member_id` in seat assignment calls
* [ ] Subscribe to `member.created`, `member.updated`, `member.deleted` webhooks

## A real-world example

Imagine you sell a **Team Pro** plan. Jane (the manager) buys 3 seats and hands them out to Alice and Bob.

### Before migration (current state)

```
Jane purchases Team Pro (3 seats)
  └── Subscription: sub_456 (customer_id: cust_jane)

Seats:
  seat_1 → customer_id: cust_alice   ← Alice is her own Customer
  seat_2 → customer_id: cust_bob     ← Bob is his own Customer
  seat_3 → unassigned

Benefit grants:
  grant for Alice → customer_id: cust_alice
  grant for Bob   → customer_id: cust_bob
```

Your code probably does something like this to grant access:

```typescript theme={"system"}
// Your current webhook handler
if (event.type === 'benefit_grant.created') {
  const userId = event.data.customer_id; // "cust_alice", the seat holder
  await grantAccessToApp(userId);
}
```

This works because `customer_id` on the grant points to Alice directly.

### After Phase 1: Preparation (non-breaking)

Members are created, `member_id` is populated on seats and grants, but `customer_id` still points to the seat holder. Your existing code **keeps working**.

```
Jane's Customer record (cust_jane)
  ├── Member: mem_jane  (role: owner)    ← new
  ├── Member: mem_alice (role: member)   ← new
  └── Member: mem_bob   (role: member)   ← new

Seats:
  seat_1 → customer_id: cust_alice, member_id: mem_alice  ← customer_id unchanged
  seat_2 → customer_id: cust_bob,   member_id: mem_bob    ← customer_id unchanged
  seat_3 → unassigned

Benefit grants:
  grant for Alice → customer_id: cust_alice, member_id: mem_alice  ← customer_id unchanged
  grant for Bob   → customer_id: cust_bob,   member_id: mem_bob    ← customer_id unchanged
```

**What you can do now:** Start reading `seat.member` and `grant.member` in your code. Both the old `customer_id` and the new `member` fields are available, so you can adapt your integration at your own pace.

```typescript theme={"system"}
// During Phase 1, both fields work
if (event.type === 'benefit_grant.created') {
  const grant = event.data;

  // Old way still works:
  grant.customer_id; // "cust_alice", still the seat holder

  // New way also works:
  grant.member.id;    // "mem_alice"
  grant.member.email; // "alice@company.com"
}
```

### After Phase 2: Member model enabled (breaking)

`customer_id` flips to the billing customer. Old seat-holder customers are deleted.

```
Jane's Customer record (cust_jane, type: "team")
  ├── Member: mem_jane  (role: owner)
  ├── Member: mem_alice (role: member)
  └── Member: mem_bob   (role: member)

cust_alice → deleted (404)
cust_bob   → deleted (404)

Seats:
  seat_1 → customer_id: cust_jane, member_id: mem_alice  ← customer_id changed!
  seat_2 → customer_id: cust_jane, member_id: mem_bob    ← customer_id changed!
  seat_3 → unassigned

Benefit grants:
  grant for Alice → customer_id: cust_jane, member_id: mem_alice  ← customer_id changed!
  grant for Bob   → customer_id: cust_jane, member_id: mem_bob    ← customer_id changed!
```

Now **every** `customer_id` is `cust_jane`, the buyer. Your old webhook handler would grant access to Jane instead of Alice and Bob.

```typescript theme={"system"}
// ❌ Broken after Phase 2 because customer_id is now the buyer
if (event.type === 'benefit_grant.created') {
  const userId = event.data.customer_id; // "cust_jane", NOT Alice!
  await grantAccessToApp(userId);
}

// ✅ Works in both phases using member to identify the actual user
if (event.type === 'benefit_grant.created') {
  const grant = event.data;
  const user = grant.member;
  await grantAccessToApp(user.id, user.email); // mem_alice, "alice@company.com"
}
```
