Billing gRPC Service Reference
Package platform.billing.v1 contains the BillingService — the single
gRPC service that manages plans, subscriptions, resource usage, limits, and
payment-provider webhooks.
BillingService is consumed by:
- API Gateway — calls
CheckLimiton the hot path before every mutating request (402 Payment Requiredwhen limit exceeded). - Billing REST handlers — proxied through Gateway for admin operations.
- Background workers —
RetrieveUsage,UpdateSubscriptionduring webhook processing and escalation timeline.
See the gRPC Overview for buf configuration, Go package conventions, deadlines, mTLS, and error mapping.
Proto files:
proto/platform/billing/v1/billing_service.proto— service definition (14 RPCs)proto/platform/billing/v1/billing_messages.proto— messages, enums, shared types
Go package: kernel.internal/platform-kernel/gen/go/platform/billing/v1;billingv1
BillingService — All RPCs
service BillingService {
// Plans
rpc CreatePlan(CreatePlanRequest) returns (CreatePlanResponse);
rpc RetrievePlan(RetrievePlanRequest) returns (RetrievePlanResponse);
rpc ListPlans(ListPlansRequest) returns (ListPlansResponse);
rpc UpdatePlan(UpdatePlanRequest) returns (UpdatePlanResponse);
// Subscriptions
rpc CreateSubscription(CreateSubscriptionRequest) returns (CreateSubscriptionResponse);
rpc RetrieveSubscription(RetrieveSubscriptionRequest) returns (RetrieveSubscriptionResponse);
rpc ListSubscriptions(ListSubscriptionsRequest) returns (ListSubscriptionsResponse);
rpc UpdateSubscription(UpdateSubscriptionRequest) returns (UpdateSubscriptionResponse);
rpc CancelSubscription(CancelSubscriptionRequest) returns (CancelSubscriptionResponse);
rpc CancelDowngrade(CancelDowngradeRequest) returns (CancelDowngradeResponse);
// Usage & Limits
rpc RetrieveUsage(RetrieveUsageRequest) returns (RetrieveUsageResponse);
rpc CheckLimit(CheckLimitRequest) returns (CheckLimitResponse);
// Webhooks
rpc HandleWebhook(HandleWebhookRequest) returns (HandleWebhookResponse);
}
Enums
SubscriptionStatus
enum SubscriptionStatus {
SUBSCRIPTION_STATUS_UNSPECIFIED = 0;
SUBSCRIPTION_STATUS_TRIALING = 1;
SUBSCRIPTION_STATUS_ACTIVE = 2;
SUBSCRIPTION_STATUS_PAST_DUE = 3;
SUBSCRIPTION_STATUS_SUSPENDED = 4;
SUBSCRIPTION_STATUS_TERMINATED = 5;
}
| Value | Numeric | State | Access level |
|---|---|---|---|
UNSPECIFIED | 0 | Default — used as "filter: all" in ListSubscriptions | — |
TRIALING | 1 | Automatic trial period on tenant creation | Full access for BILLING_TRIAL_DAYS |
ACTIVE | 2 | Payment confirmed | Full access |
PAST_DUE | 3 | Payment failed, grace period (0–7 days) | Full access |
SUSPENDED | 4 | Overdue 8–37 days; read-only | Read-only — no mutations |
TERMINATED | 5 | Overdue 38+ days | All access blocked |
Escalation timeline:
TRIALING ──► ACTIVE ──payment fails──► PAST_DUE
│
day 8 │
▼
SUSPENDED (read-only)
│
day 38 │
▼
TERMINATED (soft-delete)
Transitions are driven by the billing.worker background goroutine and by
HandleWebhook (payment success/failure events from Stripe/PayPal).
ResourceType
enum ResourceType {
RESOURCE_TYPE_UNSPECIFIED = 0;
RESOURCE_TYPE_USERS = 1;
RESOURCE_TYPE_RECORDS = 2;
RESOURCE_TYPE_STORAGE_BYTES = 3;
RESOURCE_TYPE_EVENTS_PER_DAY = 4;
RESOURCE_TYPE_MODULES = 5;
RESOURCE_TYPE_FEATURE_FLAGS = 6;
RESOURCE_TYPE_CUSTOM_DOMAINS = 7;
}
Used in CheckLimit and RetrieveUsage to scope the resource being queried.
Shared Messages
PlanLimits
Defines resource ceilings per plan. A value of 0 means unlimited.
message PlanLimits {
int64 users = 1; // Max users per tenant (0 = unlimited)
int64 records = 2; // Max Data Layer records (0 = unlimited)
int64 storage_bytes = 3; // Max file storage in bytes (0 = unlimited)
int64 events_per_day = 4; // Max events per day (0 = unlimited)
int64 modules = 5; // Max installed modules (0 = unlimited)
int64 feature_flags = 6; // Max feature flags (0 = unlimited)
int64 custom_domains = 7; // Max custom domains (0 = unlimited)
int64 included_subtenants = 8; // Child tenants included before B2B2B metered overage (0 = unlimited / metering disabled)
}
Plan
message Plan {
string id = 1;
string name = 2;
string description = 3;
int64 price_cents = 4; // Monthly price in cents (0 = free)
string currency = 5; // ISO 4217, e.g. "USD"
PlanLimits limits = 6;
repeated string features = 7; // Feature flag keys enabled by this plan
bool is_active = 8;
string created_at = 9; // ISO 8601 UTC
string updated_at = 10; // ISO 8601 UTC
}
Subscription
message Subscription {
string id = 1;
string tenant_id = 2;
string plan_id = 3;
SubscriptionStatus status = 4;
int32 version = 5; // Optimistic locking version
string current_period_start = 6; // ISO 8601 UTC
string current_period_end = 7; // ISO 8601 UTC
string trial_end = 8; // ISO 8601 UTC; empty if no trial
string canceled_at = 9; // ISO 8601 UTC; empty if not canceled
string external_id = 10; // Payment provider subscription ID
string pending_plan_id = 11; // Set when downgrade is pending
string downgrade_at = 12; // ISO 8601 UTC; when downgrade takes effect
string created_at = 13; // ISO 8601 UTC
string updated_at = 14; // ISO 8601 UTC
}
versionfield: Used for optimistic locking inUpdateSubscription. Every mutation incrementsversion. Callers must send the currentversionto prevent lost updates when multiple workers modify the same subscription concurrently (e.g., webhook processor + escalation worker).
UsageResource
message UsageResource {
int64 used = 1;
int64 limit = 2; // 0 = unlimited
float percentage = 3; // 0–100; -1 if unlimited
}
UsageReport
message UsageReport {
string tenant_id = 1;
string period_start = 2; // ISO 8601 UTC
string period_end = 3; // ISO 8601 UTC
UsageResource users = 4;
UsageResource records = 5;
UsageResource storage = 6;
UsageResource events = 7;
UsageResource modules = 8;
UsageResource feature_flags = 9;
UsageResource custom_domains = 10;
}
Plans
CreatePlan
Creates a new billing plan. Usually called by platform administrators only.
Request — CreatePlanRequest
message CreatePlanRequest {
string name = 1; // Required. Plan display name.
string description = 2; // Required. Human-readable description.
int64 price_cents = 3; // Required. Monthly price in cents (0 = free).
string currency = 4; // Required. ISO 4217.
PlanLimits limits = 5; // Required. Resource limits (0 = unlimited per field).
repeated string features = 6; // Optional. Feature flag keys enabled by this plan.
}
Response — CreatePlanResponse
message CreatePlanResponse {
Plan plan = 1;
}
gRPC errors:
| gRPC status | Condition |
|---|---|
ALREADY_EXISTS | Plan name already exists |
INVALID_ARGUMENT | Missing required field or invalid currency code |
RetrievePlan
Request — RetrievePlanRequest
message RetrievePlanRequest {
string id = 1; // Required. Plan UUID v7.
}
Response — RetrievePlanResponse
message RetrievePlanResponse {
Plan plan = 1;
}
ListPlans
Request — ListPlansRequest
message ListPlansRequest {
platform.common.v1.PaginationRequest pagination = 1;
bool include_inactive = 2; // Default: false — only active plans
}
Response — ListPlansResponse
message ListPlansResponse {
repeated Plan data = 1;
platform.common.v1.PaginationMeta meta = 2;
}
UpdatePlan
Partial update — only optional fields set in the request are changed.
Request — UpdatePlanRequest
message UpdatePlanRequest {
string id = 1; // Required. Plan UUID v7.
optional string name = 2;
optional string description = 3;
optional int64 price_cents = 4;
PlanLimits limits = 5; // null = no change; non-null replaces all limit fields
repeated string features = 6; // Replaces entire feature list when provided
optional bool is_active = 7;
}
Response — UpdatePlanResponse
message UpdatePlanResponse {
Plan plan = 1;
}
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | Plan ID does not exist |
FAILED_PRECONDITION | Cannot deactivate a plan with active subscribers |
Subscriptions
CreateSubscription
Creates a subscription for a tenant. Called automatically by the platform
on tenant provisioning (default: Free plan, TRIALING status).
Request — CreateSubscriptionRequest
message CreateSubscriptionRequest {
string tenant_id = 1; // Required. UUID of the tenant.
string plan_id = 2; // Required. UUID of the plan.
string external_id = 3; // Optional. Payment provider subscription ID.
}
Response — CreateSubscriptionResponse
message CreateSubscriptionResponse {
Subscription subscription = 1;
}
gRPC errors:
| gRPC status | Condition |
|---|---|
ALREADY_EXISTS | Tenant already has an active subscription |
NOT_FOUND | tenant_id or plan_id does not exist |
RetrieveSubscription
Request — RetrieveSubscriptionRequest
message RetrieveSubscriptionRequest {
string id = 1; // Required. Subscription UUID v7.
}
Response — RetrieveSubscriptionResponse
message RetrieveSubscriptionResponse {
Subscription subscription = 1;
}
ListSubscriptions
Request — ListSubscriptionsRequest
message ListSubscriptionsRequest {
platform.common.v1.PaginationRequest pagination = 1;
string tenant_id = 2; // Optional. Filter by tenant UUID.
SubscriptionStatus status = 3; // Optional. Filter by status (UNSPECIFIED = all).
}
Response — ListSubscriptionsResponse
message ListSubscriptionsResponse {
repeated Subscription data = 1;
platform.common.v1.PaginationMeta meta = 2;
}
UpdateSubscription
Changes the plan for a tenant. Implements optimistic locking via the
version field — version must match the current value in the database.
Request — UpdateSubscriptionRequest
message UpdateSubscriptionRequest {
string id = 1; // Required. Subscription UUID v7.
string plan_id = 2; // Required. UUID of the new plan.
int32 version = 3; // REQUIRED. Must match current subscription.version.
}
Response — UpdateSubscriptionResponse
message UpdateSubscriptionResponse {
Subscription subscription = 1; // Contains incremented version.
}
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | Subscription or plan ID does not exist |
ABORTED | version mismatch — concurrent modification detected; caller should re-read and retry |
FAILED_PRECONDITION | Subscription is in TERMINATED status — cannot be updated |
Downgrade scheduling: If the new plan has lower limits than the current plan, the system creates a
pending_plan_idanddowngrade_atentry instead of an immediate switch. The actual downgrade happens at the next billing period boundary.CancelDowngradereverts this.
CancelSubscription
Marks the subscription for cancellation at the end of the current period. Access is blocked immediately only after the period ends.
Request — CancelSubscriptionRequest
message CancelSubscriptionRequest {
string id = 1; // Required. Subscription UUID v7.
}
Response — CancelSubscriptionResponse
message CancelSubscriptionResponse {} // Empty — success signals cancellation scheduled
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | Subscription ID does not exist |
FAILED_PRECONDITION | Subscription already canceled or terminated |
CancelDowngrade
Reverts a pending plan downgrade, restoring the original plan.
Only effective when pending_plan_id is set on the subscription.
Request — CancelDowngradeRequest
message CancelDowngradeRequest {
string id = 1; // Required. Subscription UUID v7.
}
Response — CancelDowngradeResponse
message CancelDowngradeResponse {
Subscription subscription = 1; // pending_plan_id and downgrade_at cleared.
}
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | Subscription ID does not exist |
FAILED_PRECONDITION | No pending downgrade to cancel |
Usage & Limits
RetrieveUsage
Returns the full resource usage report for a tenant for the current billing period.
Request — RetrieveUsageRequest
message RetrieveUsageRequest {
string tenant_id = 1; // Required. Tenant UUID v7.
}
Response — RetrieveUsageResponse
message RetrieveUsageResponse {
UsageReport report = 1;
}
CheckLimit
Hot-path RPC. Called by the Gateway before every mutating request
(POST, PATCH, DELETE) to verify the tenant has not exceeded its plan
limits. Must be sub-millisecond — results are cached in Valkey at the
Gateway layer.
Gateway receives POST /api/v1/data/crm/contacts
│
▼
CheckLimit(tenant_id, RESOURCE_TYPE_RECORDS)
│
├─ allowed: true → forward to Data Layer
└─ allowed: false → 402 Payment Required
Request — CheckLimitRequest
message CheckLimitRequest {
string tenant_id = 1; // Required. Tenant UUID v7.
ResourceType resource = 2; // Required. The resource type being consumed.
}
Response — CheckLimitResponse
message CheckLimitResponse {
ResourceType resource = 1;
bool allowed = 2; // false if limit exceeded
int64 current_usage = 3;
int64 limit = 4; // 0 = unlimited
int64 remaining = 5; // -1 if unlimited
}
| Field | Description |
|---|---|
allowed | true → proceed; false → Gateway returns 402 Payment Required |
limit | 0 means unlimited — allowed is always true in this case |
remaining | -1 means unlimited |
Valkey caching (Gateway-side):
| Parameter | Value |
|---|---|
| Cache key | limit:{tenantId}:{resourceType} |
| TTL | 5 minutes (BILLING_LIMIT_CACHE_TTL_SEC=300) |
| Invalidation | Kafka event billing.plan.changed or billing.subscription.changed → Gateway cache-invalidation worker flushes the key |
Webhooks
HandleWebhook
Provider-agnostic webhook ingestion. The IAM service receives the raw HTTP
request body and headers from Gateway, dispatches to the correct registered
WebhookVerifier adapter (Stripe, PayPal, etc.), verifies the signature,
and processes the event idempotently.
Request — HandleWebhookRequest
message HandleWebhookRequest {
string provider = 1; // e.g. "stripe", "paypal". Matches adapter registry key.
bytes raw_body = 2; // Raw HTTP request body (for signature verification).
map<string, string> headers = 3; // HTTP headers including provider signature header.
}
| Field | Notes |
|---|---|
provider | Must match a registered adapter key. Unknown provider → NOT_FOUND. |
raw_body | The raw bytes before any JSON parsing — signature verification requires the exact original bytes. |
headers | Includes Stripe-Signature, PayPal-Transmission-Sig, etc. |
Response — HandleWebhookResponse
message HandleWebhookResponse {
bool processed = 1; // true if the event was processed; false if already seen (idempotent)
string event_id = 2; // Provider event ID (used for idempotency dedup in PostgreSQL)
string event_type = 3; // e.g. "invoice.payment_succeeded", "subscription.canceled"
}
Idempotency: Events are deduplicated by event_id in the webhook_events
PostgreSQL table. Replayed webhooks return processed: false without side
effects.
Supported webhook events:
| Provider event | Billing action |
|---|---|
invoice.payment_succeeded | → PAST_DUE → ACTIVE |
invoice.payment_failed | → ACTIVE → PAST_DUE |
customer.subscription.deleted | → CancelSubscription |
customer.subscription.updated | → UpdateSubscription (plan change) |
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | Unknown provider — no adapter registered |
PERMISSION_DENIED | Signature verification failed |
INVALID_ARGUMENT | Malformed raw_body or missing signature header |
Billing Permissions (RBAC)
| Permission key | Scope |
|---|---|
billing.plan.change | Change subscription plan (upgrade/downgrade) |
billing.payment.manage | Manage payment methods |
billing.info.update | Update billing contact information |
billing.view | View subscription status and usage |
billing.export | Export billing history and invoices |
These permissions are enforced at the Gateway REST layer, not at the gRPC level — the gRPC service trusts its callers (mTLS-authenticated Gateway).