Installation
composer require crovver/crovver-php
Requirements: PHP 8.0+, Composer.
Initialize the Client
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
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
$plans = $client->getPlans();
foreach ($plans->plans as $plan) {
echo $plan->name . ' - $' . ($plan->priceAmount / 100) . '/mo';
}
Subscriptions
$result = $client->getSubscriptions('workspace_abc123');
foreach ($result->subscriptions as $sub) {
echo $sub->status; // active, trial, past_due, pending_cancel, canceled, expired
}
Feature Entitlements
// 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
$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.
$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
$addons = $client->listAvailableAddons('workspace_abc123');
foreach ($addons as $addon) {
echo $addon['name'] . ': ' . $addon['creditQty'] . ' credits — '
. $addon['currency'] . ' ' . $addon['amount'];
}
Purchase an add-on
$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
$active = $client->getActiveAddonCredits('workspace_abc123');
foreach ($active as $pool) {
echo $pool['poolKey'] . ': ' . $pool['remaining'] . ' remaining';
}
Usage Tracking (deprecated)
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 instead.
// ❌ 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
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
$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.
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:
$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).
bulkAllocateSeats is never retried automatically to prevent duplicate allocations on network errors.
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.
// 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.
$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 |
Proration checkout is never retried automatically to prevent duplicate Stripe sessions.
Error Handling
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
}
5xx errors, 429, and 408 responses are retried automatically with exponential backoff. Checkout endpoints are never retried to prevent duplicate charges.