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

# PHP SDK

> Integrate Crovver into your PHP backend.

## Installation

```bash theme={null}
composer require crovver/crovver-php
```

**Requirements:** PHP 8.0+, Composer.

## Initialize the Client

```php theme={null}
use Crovver\CrovverClient;
use Crovver\CrovverConfig;

$client = new CrovverClient(new CrovverConfig(
    apiKey: getenv('CROVVER_SECRET_KEY'),
));
```

### Configuration Options

| Option       | Type     | Default  | Description                         |
| ------------ | -------- | -------- | ----------------------------------- |
| `apiKey`     | string   | required | Your secret API key (`sk_live_...`) |
| `timeout`    | int      | `30`     | Request timeout in seconds          |
| `maxRetries` | int      | `3`      | Retry attempts for server errors    |
| `debug`      | bool     | `false`  | Log requests and responses          |
| `logger`     | callable | `null`   | Custom logger: `fn($msg, $ctx)`     |

## Tenant Management

```php theme={null}
use Crovver\Types\CreateTenantRequest;

// Create a tenant (B2B: workspace or user)
$tenant = $client->createTenant(new CreateTenantRequest(
    externalTenantId: 'workspace_abc123',
    name: 'Acme Corp',
    externalUserId: 'user_123',
));

// Retrieve a tenant
$tenant = $client->getTenant('workspace_abc123');
```

## Plans

```php theme={null}
$plans = $client->getPlans();

foreach ($plans->plans as $plan) {
    echo $plan->name . ' - $' . ($plan->priceAmount / 100) . '/mo';
}
```

## Subscriptions

```php theme={null}
$result = $client->getSubscriptions('workspace_abc123');

foreach ($result->subscriptions as $sub) {
    echo $sub->status; // active, trial, past_due, pending_cancel, canceled, expired
}
```

## Feature Entitlements

```php theme={null}
// Single-product org
$canAccess = $client->canAccess('workspace_abc123', 'advanced_analytics');

// Multi-product: scope to a specific product
$canAccess = $client->canAccess('workspace_abc123', 'advanced_analytics', 'product-a');

// Subscription-existence check — does this tenant have any active sub for product-a?
$hasProduct = $client->canAccess('workspace_abc123', '', 'product-a');
```

## Credits

Credits are the preferred way to track and gate consumption in Crovver. Each credit pool has a key (e.g. `"ai_generations"`) and is refilled by plan entitlements or add-on purchases.

### Check balance

```php theme={null}
$balance = $client->getCreditsBalance('workspace_abc123');

foreach ($balance['pools'] as $pool) {
    echo $pool['poolKey'] . ': ' . $pool['remaining'] . ' remaining';
}
```

### Consume credits

`consumeCredits` is idempotent — passing the same `$idempotencyKey` twice returns the original result without double-deducting.

```php theme={null}
$result = $client->consumeCredits(
    tenantId: 'workspace_abc123',
    poolKey: 'ai_generations',
    amount: 1,
    idempotencyKey: bin2hex(random_bytes(16)), // stable per user action
    metadata: ['prompt' => 'summarise report'], // optional
);

if ($result->result === 'success') {
    echo 'Remaining: ' . $result->remaining;
} else {
    // result === 'insufficient_credits'
    // Prompt the user to purchase an add-on or upgrade
}
```

**`consumeCredits` parameters:**

| Parameter        | Type        | Required | Description                                      |
| ---------------- | ----------- | -------- | ------------------------------------------------ |
| `tenantId`       | string      | Yes      | Your tenant/workspace ID                         |
| `poolKey`        | string      | Yes      | Credit pool identifier defined in your plan      |
| `amount`         | int         | Yes      | Units to deduct                                  |
| `idempotencyKey` | string      | Yes      | Unique key per action; prevents double-deduction |
| `metadata`       | array\|null | No       | Arbitrary key/value pairs stored with the event  |

**`ConsumeResponse` fields:**

| Field              | Type   | Description                                      |
| ------------------ | ------ | ------------------------------------------------ |
| `result`           | string | `"success"` or `"insufficient_credits"`          |
| `remaining`        | int    | Credits left in the pool after deduction         |
| `alreadyProcessed` | bool   | `true` when the idempotency key was already seen |
| `poolKey`          | string | Echoed pool key                                  |

***

## Add-ons

Add-ons let tenants purchase extra credit packs without changing their base plan.

### List available add-ons

```php theme={null}
$addons = $client->listAvailableAddons('workspace_abc123');

foreach ($addons as $addon) {
    echo $addon['name'] . ': ' . $addon['creditQty'] . ' credits — '
        . $addon['currency'] . ' ' . $addon['amount'];
}
```

### Purchase an add-on

```php theme={null}
$purchase = $client->purchaseAddon(
    externalTenantId: 'workspace_abc123',
    addonId: 'addon_uuid',
    currency: 'USD',
    idempotencyKey: bin2hex(random_bytes(16)),
    successUrl: 'https://yourapp.com/billing/success',
    cancelUrl: 'https://yourapp.com/billing/cancel',
);

if ($purchase->requiresPayment) {
    // Redirect to Stripe checkout
    header('Location: ' . $purchase->checkoutUrl);
    exit;
} else {
    // Credits added immediately (free add-on or already processed)
    echo $purchase->creditQty . ' credits added';
}
```

**`AddonPurchaseResponse` fields:**

| Field              | Type         | Description                                                    |
| ------------------ | ------------ | -------------------------------------------------------------- |
| `purchaseId`       | string       | Unique purchase record ID                                      |
| `checkoutUrl`      | string\|null | Stripe checkout URL — present when `requiresPayment` is `true` |
| `requiresPayment`  | bool         | `true` when a checkout session was created                     |
| `addonName`        | string       | Display name of the add-on                                     |
| `creditQty`        | int          | Credits that will be added on completion                       |
| `amount`           | float        | Charge amount in major currency units                          |
| `currency`         | string       | Currency code (e.g. `"USD"`)                                   |
| `alreadyProcessed` | bool         | `true` when the idempotency key was already seen               |

### Get active add-on credits

```php theme={null}
$active = $client->getActiveAddonCredits('workspace_abc123');

foreach ($active as $pool) {
    echo $pool['poolKey'] . ': ' . $pool['remaining'] . ' remaining';
}
```

***

## Usage Tracking (deprecated)

<Warning>
  `recordUsage` and `checkUsageLimit` are deprecated. They write to a simple event log with no idempotency and do not integrate with the credit pool system. Use [`consumeCredits`](#credits) instead.
</Warning>

```php theme={null}
// ❌ Deprecated — use consumeCredits() instead
$client->recordUsage('workspace_abc123', 'api_calls', 1);

// ❌ Deprecated — use getCreditsBalance() instead
$limit = $client->checkUsageLimit('workspace_abc123', 'api_calls');
echo $limit->current . ' / ' . $limit->limit;
```

***

## Checkout

```php theme={null}
use Crovver\Types\CreateCheckoutSessionRequest;

$session = $client->createCheckoutSession(new CreateCheckoutSessionRequest(
    externalTenantId: 'workspace_abc123',
    planId: 'plan_pro',
    currency: 'USD',                                    // optional; defaults to plan's first price
    successUrl: 'https://yourapp.com/billing/success',
    cancelUrl: 'https://yourapp.com/billing/cancel',
));

// Redirect user to $session->checkoutUrl
```

## Seat Management

Use these methods on seat-based plans to track which users occupy seats. This is separate from `recordUsage` — seat allocation is for per-user billing, not credit/quota consumption.

### Get Seat Count

```php theme={null}
$count = $client->getSeatCount('workspace_abc123');

echo $count->activeCount;           // Active allocated seats
echo $count->capacityUnits;         // Total seats in subscription
echo $count->utilizationPercentage; // activeCount / capacityUnits × 100
echo $count->billingMode;           // recurring | manual | free
echo $count->exceeded ? 'Over limit' : 'Within limit';
```

### Allocate a Seat

Call this when a user joins a workspace to record their seat allocation.

```php theme={null}
use Crovver\Types\AllocateSeatRequest;

$result = $client->allocateSeat(new AllocateSeatRequest(
    requestingEntityId: 'workspace_abc123',
    externalUserId: 'user_456',
    email: 'alice@acme.com',
    name: 'Alice',
));

if ($result->success) {
    echo 'Seat allocated. Active seats: ' . $result->capacity->activeCount;
}
```

If the workspace is at capacity and the plan is seat-based, Crovver returns a proration preview instead of allocating the seat:

```php theme={null}
$result = $client->allocateSeat(new AllocateSeatRequest(
    requestingEntityId: 'workspace_abc123',
    externalUserId: 'user_789',
));

if ($result->requiresProration) {
    // Show the user the upgrade cost before proceeding
    echo 'Adding this seat costs ' . $result->proration->amount . ' ' . $result->proration->currency;
    echo ' for ' . $result->proration->daysRemaining . ' remaining days in the billing period.';

    // Then call createProrationCheckout to start payment
    $checkout = $client->createProrationCheckout(
        externalTenantId: 'workspace_abc123',
        newCapacity: $result->proration->newSeats,
        successUrl: 'https://yourapp.com/seats?upgraded=1',
        cancelUrl: 'https://yourapp.com/seats',
    );

    // Redirect to $checkout->checkoutUrl
}
```

#### `AllocateSeatRequest` Parameters

| Parameter            | Type   | Required | Description                                                                                                                              |
| -------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `requestingEntityId` | string | yes      | Your tenant/workspace ID                                                                                                                 |
| `externalUserId`     | string | yes      | Your user ID. Must be non-empty. The call is idempotent — re-allocating the same ID updates the record rather than creating a duplicate. |
| `email`              | string | no       | User's email for audit records                                                                                                           |
| `name`               | string | no       | User's display name for audit records                                                                                                    |
| `metadata`           | array  | no       | Arbitrary key/value pairs stored with the allocation                                                                                     |

#### `AllocateSeatResponse` Fields

| Field               | Type                          | Description                                                                     |
| ------------------- | ----------------------------- | ------------------------------------------------------------------------------- |
| `success`           | bool                          | `true` when a seat was recorded                                                 |
| `requiresProration` | bool                          | `true` when capacity is full and a prorated upgrade is needed                   |
| `requiresCheckout`  | bool                          | `true` when proration was already confirmed but payment is still pending (rare) |
| `capacity`          | `AllocateSeatCapacity\|null`  | Present on success: `activeCount`, `capacityUnits`, `exceeded`                  |
| `proration`         | `AllocateSeatProration\|null` | Present when `requiresProration` is true                                        |

#### `GetSeatCountResponse` Fields

| Field                   | Type   | Description                              |
| ----------------------- | ------ | ---------------------------------------- |
| `activeCount`           | int    | Number of active seat allocations        |
| `capacityUnits`         | int    | Total seats included in the subscription |
| `utilizationPercentage` | int    | `activeCount / capacityUnits × 100`      |
| `billingMode`           | string | `recurring`, `manual`, or `free`         |
| `exceeded`              | bool   | Whether `activeCount > capacityUnits`    |

### Bulk Allocate Seats

Allocate up to 100 users in one atomic call. Users beyond `capacity_units` are returned in `rejected` — no proration is triggered. Already-active users are silently skipped (idempotent).

<Warning>
  `bulkAllocateSeats` is **never retried automatically** to prevent duplicate allocations on network errors.
</Warning>

```php theme={null}
use Crovver\Types\BulkAllocateSeatsRequest;
use Crovver\Types\BulkAllocateSeatUser;

$result = $client->bulkAllocateSeats(new BulkAllocateSeatsRequest(
    requestingEntityId: 'workspace_abc123',
    users: [
        new BulkAllocateSeatUser('user_1', 'alice@acme.com', 'Alice'),
        new BulkAllocateSeatUser('user_2', 'bob@acme.com',   'Bob'),
        new BulkAllocateSeatUser('user_3', 'carol@acme.com', 'Carol'),
    ],
    metadata: ['addedBy' => 'admin_789'], // optional
));

echo count($result->allocated) . ' allocated';
echo count($result->skipped)   . ' skipped (already active)';
echo count($result->rejected)  . ' rejected (over capacity)';

echo $result->capacity->activeCount . ' / ' . $result->capacity->capacityUnits . ' seats';

if (count($result->rejected) > 0) {
    // Call createProrationCheckout to upgrade capacity, then retry $result->rejected
    echo $result->message;
}
```

Returns HTTP **207** when at least one user was rejected; **200** otherwise.

#### `BulkAllocateSeatsRequest` Parameters

| Parameter            | Type                     | Required | Description                                                              |
| -------------------- | ------------------------ | -------- | ------------------------------------------------------------------------ |
| `requestingEntityId` | string                   | yes      | Your tenant/workspace ID                                                 |
| `users`              | `BulkAllocateSeatUser[]` | yes      | Max 100 per call. Each `externalUserId` must be unique within the batch. |
| `metadata`           | array                    | no       | Attached to every allocation created in this batch                       |

#### `BulkAllocateSeatUser` Parameters

| Parameter        | Type   | Required | Description         |
| ---------------- | ------ | -------- | ------------------- |
| `externalUserId` | string | yes      | Your app's user ID  |
| `email`          | string | no       | User's email        |
| `name`           | string | no       | User's display name |

#### `BulkAllocateSeatsResponse` Fields

| Field                     | Type           | Description                            |
| ------------------------- | -------------- | -------------------------------------- |
| `allocated`               | `string[]`     | User IDs inserted this call            |
| `skipped`                 | `string[]`     | Already active — not re-inserted       |
| `rejected`                | `string[]`     | Would exceed capacity — not inserted   |
| `capacity->activeCount`   | int            | Active seat count after this operation |
| `capacity->capacityUnits` | int            | Total seat limit on the subscription   |
| `message`                 | `string\|null` | Present when users were rejected       |

***

### List Allocations

Returns a paginated list of users allocated to the tenant's active subscription, along with a real-time capacity summary.

```php theme={null}
// Active users, first page (defaults)
$result = $client->getAllocations('workspace_abc123');

foreach ($result->allocations as $user) {
    echo $user->externalUserId . ' — ' . $user->email . ' (' . $user->status . ')';
    echo ' since ' . $user->allocatedAt;
}

// Capacity summary
echo $result->capacity->activeCount . ' / ' . $result->capacity->capacityUnits . ' seats';
echo $result->capacity->utilizationPercentage . '% utilized';

if ($result->capacity->exceeded) {
    // Prompt upgrade
}

// Pagination
echo $result->pagination->total . ' total — page '
    . $result->pagination->page . ' of ' . $result->pagination->totalPages;

// Page through all users including removed
$page2 = $client->getAllocations(
    requestingEntityId: 'workspace_abc123',
    status: 'all',    // 'active' | 'removed' | 'all'
    page: 2,
    limit: 25,        // max 100, default 50
);
```

#### `getAllocations` Parameters

| Parameter            | Type   | Default    | Description                         |
| -------------------- | ------ | ---------- | ----------------------------------- |
| `requestingEntityId` | string | required   | Your tenant/workspace ID            |
| `status`             | string | `'active'` | `'active'`, `'removed'`, or `'all'` |
| `page`               | int    | `1`        | 1-based page number                 |
| `limit`              | int    | `50`       | Page size, max 100                  |

#### `AllocationUser` Fields

| Field            | Type           | Description                           |
| ---------------- | -------------- | ------------------------------------- |
| `externalUserId` | string         | Your app's user ID                    |
| `email`          | `string\|null` | User's email                          |
| `name`           | `string\|null` | User's display name                   |
| `status`         | string         | `'active'` or `'removed'`             |
| `allocatedAt`    | string         | ISO 8601 timestamp of seat assignment |
| `removedAt`      | `string\|null` | ISO 8601 timestamp of deallocation    |
| `metadata`       | array          | Metadata stored at allocation time    |

***

### Proration Checkout

Call this whenever a tenant needs more seats — proactively from a settings page, or after presenting the preview returned by `allocateSeat()`. Crovver calculates the prorated charge for the remaining days in the billing period and creates a checkout session using the same payment provider as the tenant's original subscription.

```php theme={null}
$checkout = $client->createProrationCheckout(
    externalTenantId: 'workspace_abc123',
    newCapacity: 25,
    successUrl: 'https://yourapp.com/seats?upgraded=1',
    cancelUrl: 'https://yourapp.com/seats',
);

if ($checkout->requiresPayment) {
    // Redirect to the provider's checkout page
    header('Location: ' . $checkout->checkoutUrl);
} else {
    // Seat count was updated without payment (e.g. within free allowance)
    echo $checkout->message;
}
```

#### `createProrationCheckout` Parameters

| Parameter          | Type   | Required | Description                                                    |
| ------------------ | ------ | -------- | -------------------------------------------------------------- |
| `externalTenantId` | string | yes      | Your tenant/workspace ID                                       |
| `newCapacity`      | int    | yes      | The new total seat count (must exceed current `capacityUnits`) |
| `planId`           | string | no       | Plan ID if also switching plans at the same time               |
| `successUrl`       | string | no       | Redirect URL after successful payment                          |
| `cancelUrl`        | string | no       | Redirect URL if the user cancels                               |

#### `ProrationCheckoutResponse` Fields

| Field              | Type         | Description                                                                   |
| ------------------ | ------------ | ----------------------------------------------------------------------------- |
| `prorationId`      | string       | Unique ID for this proration calculation                                      |
| `requiresPayment`  | bool         | `true` when a Stripe checkout session was created                             |
| `checkoutUrl`      | string\|null | Stripe checkout URL — redirect the user here when `requiresPayment` is `true` |
| `prorationAmount`  | float        | Amount charged in the plan's currency (in major units, e.g. `17.50`)          |
| `prorationDetails` | array        | Breakdown: `daysRemaining`, `totalDaysInPeriod`, `perSeatPrice`, etc.         |
| `message`          | string       | Human-readable summary of the proration                                       |

<Note>
  Proration checkout is **never** retried automatically to prevent duplicate Stripe sessions.
</Note>

## Error Handling

```php theme={null}
use Crovver\CrovverError;

try {
    $result = $client->getSubscriptions('workspace_abc123');
} catch (CrovverError $e) {
    $e->getMessage();     // Human-readable message
    $e->getStatusCode();  // HTTP status code (null for network errors)
    $e->getErrorCode();   // API error code string
    $e->isRetryable();    // Whether the SDK already retried
}
```

<Note>
  5xx errors, 429, and 408 responses are retried automatically with exponential backoff. Checkout endpoints are **never** retried to prevent duplicate charges.
</Note>
