Skip to main content

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 CheckLimit on the hot path before every mutating request (402 Payment Required when limit exceeded).
  • Billing REST handlers — proxied through Gateway for admin operations.
  • Background workersRetrieveUsage, UpdateSubscription during 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;
}
ValueNumericStateAccess level
UNSPECIFIED0Default — used as "filter: all" in ListSubscriptions
TRIALING1Automatic trial period on tenant creationFull access for BILLING_TRIAL_DAYS
ACTIVE2Payment confirmedFull access
PAST_DUE3Payment failed, grace period (0–7 days)Full access
SUSPENDED4Overdue 8–37 days; read-onlyRead-only — no mutations
TERMINATED5Overdue 38+ daysAll 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
}

version field: Used for optimistic locking in UpdateSubscription. Every mutation increments version. Callers must send the current version to 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 statusCondition
ALREADY_EXISTSPlan name already exists
INVALID_ARGUMENTMissing 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 statusCondition
NOT_FOUNDPlan ID does not exist
FAILED_PRECONDITIONCannot 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 statusCondition
ALREADY_EXISTSTenant already has an active subscription
NOT_FOUNDtenant_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 statusCondition
NOT_FOUNDSubscription or plan ID does not exist
ABORTEDversion mismatch — concurrent modification detected; caller should re-read and retry
FAILED_PRECONDITIONSubscription 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_id and downgrade_at entry instead of an immediate switch. The actual downgrade happens at the next billing period boundary. CancelDowngrade reverts 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 statusCondition
NOT_FOUNDSubscription ID does not exist
FAILED_PRECONDITIONSubscription 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 statusCondition
NOT_FOUNDSubscription ID does not exist
FAILED_PRECONDITIONNo 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
}
FieldDescription
allowedtrue → proceed; false → Gateway returns 402 Payment Required
limit0 means unlimited — allowed is always true in this case
remaining-1 means unlimited

Valkey caching (Gateway-side):

ParameterValue
Cache keylimit:{tenantId}:{resourceType}
TTL5 minutes (BILLING_LIMIT_CACHE_TTL_SEC=300)
InvalidationKafka 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.
}
FieldNotes
providerMust match a registered adapter key. Unknown provider → NOT_FOUND.
raw_bodyThe raw bytes before any JSON parsing — signature verification requires the exact original bytes.
headersIncludes 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 eventBilling action
invoice.payment_succeededPAST_DUEACTIVE
invoice.payment_failedACTIVEPAST_DUE
customer.subscription.deletedCancelSubscription
customer.subscription.updatedUpdateSubscription (plan change)

gRPC errors:

gRPC statusCondition
NOT_FOUNDUnknown provider — no adapter registered
PERMISSION_DENIEDSignature verification failed
INVALID_ARGUMENTMalformed raw_body or missing signature header

Billing Permissions (RBAC)

Permission keyScope
billing.plan.changeChange subscription plan (upgrade/downgrade)
billing.payment.manageManage payment methods
billing.info.updateUpdate billing contact information
billing.viewView subscription status and usage
billing.exportExport 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).