Stripe Billing + API Key Enforcement (v1)
This document defines the production billing path implemented in Sentinel Signal for trial-key issuance, checkout, plan activation, API key enforcement, usage batching, and free-tier gating.
Commercial model
- Product:
Sentinel Signal API - Billing model: usage-based metered pricing per scoring call
- Trial/free tier: first 1,000 calls per month (enforced in API runtime)
- Paid tiers:
- Growth: first 1,000 calls/month included, then
$0.003per scoring call - Pro:
$99/monthincludes first50,000calls, then graduated metered overage pricing
Design intent:
- no seats
- no contract sprawl
- deterministic per-call billing for automation workloads
Billing Unit Definition (Authoritative)
Billing unit:
- one successful scoring response from a scoring endpoint
- scored endpoints in scope:
POST /v1/scorePOST /v1/claims/denial/scorePOST /v1/claims/prior-auth/predictPOST /v1/claims/reimbursement/estimate
Billable outcome policy:
- billable: HTTP
2xxscoring responses - not billable: HTTP
4xx/5xx/ transport failures (including401,402,409,429) - not billable: telemetry endpoint calls such as
POST /v1/feedback
Retry/idempotency billing policy:
- retries are billed when they produce an additional successful
2xxscoring response - repeated submissions of the same claim payload are treated as distinct billable scoring calls
- application-level deduplication (if desired) must be implemented by the calling system
1. Exact Stripe Checkout Integration Flow
- Create checkout session:
- Endpoint:
POST /v1/billing/checkout/session - Auth:
X-Admin-Token - Inputs:
email,plan(growthorpro), optionalsuccess_url,cancel_url - Behavior:
- Upsert billing account in
rcm.billing_accounts - Create/reuse Stripe customer
- Create Stripe checkout session (
mode=subscription) - Persist session metadata in
rcm.billing_checkout_sessions - Return
checkout_session_idand redirectcheckout_url
Portal self-serve path (preferred for customers):
POST /v1/control-plane/workspace/billing/checkout/session(dashboard session auth)GET /v1/control-plane/workspace/billing/status(dashboard session billing snapshot)- served via
/portal/dashboard
Trial same-key upgrade path (no signup required):
POST /v1/keys/trial(no auth; returnsss_trial_...)POST /v1/billing/checkout-session(Bearer trial API key; acceptsprice_idorplan)- Stripe webhook upgrades the same billing account/key in place (no key migration)
- Customer completes Stripe Checkout:
- Stripe redirects browser to
success_url - Stripe sends webhook events to
POST /v1/billing/webhooks/stripe
- Webhook verification + idempotent processing:
- Endpoint verifies
Stripe-Signaturevia HMAC SHA256 and timestamp tolerance - Event id is inserted into
rcm.billing_webhook_events(ON CONFLICT DO NOTHING) to guarantee idempotency - Duplicate event ids are acknowledged as already processed and skipped (no re-provisioning side effects)
- Subscription/account updates persist Stripe event markers (
stripe_last_event_created,stripe_last_event_id) and ignore stale/out-of-order events - Provisioning path never auto-mints multiple API keys for the same subscription lifecycle event
- Supported events update account/subscription state:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paid(andinvoice.payment_succeededalias)invoice.payment_failed
- Issue API key after account activation:
- Endpoint:
POST /v1/apikeys/issue - Auth:
X-Admin-Token - Validates plan/status policy for paid plans
- Stores hashed key in
rcm.api_keys - Returns cleartext key once (not recoverable after issuance)
2. API Key Database Schema + Enforcement Logic
Schema
rcm.billing_accounts- Plan, status, Stripe customer/subscription, period bounds, limit overrides
rcm.api_keys- Key hash, prefix, scopes, lifecycle fields (
active,revoked_at,expires_at) rcm.control_plane_setup_links- One-time setup-link token hashes for self-serve dashboard onboarding
rcm.control_plane_sessions- Revocable dashboard sessions scoped to a single billing account/workspace
rcm.billing_checkout_sessions- Checkout session lifecycle and Stripe mapping
rcm.billing_webhook_events- Webhook idempotency + processing audit
rcm.api_key_usage_monthly- Monthly per-workflow usage aggregates
Migrations:
sql/migrations/0004_add_billing_and_api_keys.sqlsql/migrations/0009_add_control_plane_setup_and_sessions.sqlsql/migrations/0010_add_control_plane_identities.sqlsql/migrations/0011_add_trial_keys.sql
Enforcement
Bearer auth now supports two modes:
- JWT mode (existing)
- Signed JWT from token service (
HS256) - Scope check remains unchanged
- API key mode (new)
- If JWT decode fails, bearer token is evaluated as API key
- API key is hashed with
API_KEY_HASH_SECRETand matched againstrcm.api_keys.key_hash - Enforced checks:
- key active and not revoked
- key not expired
- billing account status is active/trialing
- required scope present
- non-active billing returns
402 Payment Required
3. Usage Batching + Reporting Service Design
Runtime batcher
Implemented in app/usage_metering.py.
- In-memory reservation path per scoring call
- Per-key rolling 1-second window for RPS enforcement
- Pending usage is batched and flushed to Postgres periodically
- Flush trigger:
- batch size threshold (
USAGE_BATCH_SIZE), or - periodic interval (
USAGE_FLUSH_INTERVAL_SECONDS) - Optional Stripe usage reporting is batched separately and posted periodically
STRIPE_USAGE_REPORT_INTERVAL_SECONDS(default hourly behavior)- Meter Events are used for meter-backed Stripe prices; legacy subscription-item usage records are retained as a fallback for legacy metered prices without a meter
Source of truth and reconciliation
Operational source of truth:
- for runtime gating and usage endpoints: Postgres aggregates in
rcm.api_key_usage_monthlyplus in-memory pending counters - for invoicing:
- Stripe Meter Events for meter-backed prices (current Stripe Dashboard default)
- legacy Stripe subscription-item usage records for older metered prices without meters (compatibility fallback)
Partial-failure behavior:
- DB updated, Stripe usage post failed:
- DB aggregate remains advanced
- usage quantity remains queued for Stripe retry on subsequent report intervals
- Stripe usage post succeeded, DB update delayed:
- current implementation posts to Stripe only after DB upsert succeeds; this ordering avoids Stripe-ahead-of-DB under normal flow
- crash windows can still produce drift and must be covered by reconciliation
Recommended reconciliation control:
- daily reconciliation job compares
rcm.api_key_usage_monthly.billable_countagainst Stripe period usage totals - alert on absolute or relative drift above threshold (example:
> 1%or> 100calls, whichever is larger) - investigate and replay missing usage batches before invoice finalization
Invoice finalization timing
- reconciliation runs daily during the billing period
- reconciliation runs again immediately before invoice finalization
- any missing usage batches are replayed before invoices are closed
Storage model
Batched writes upsert into:
rcm.api_key_usage_monthly (api_key_id, usage_month, workflow)
Counters tracked:
request_countbillable_countsuccess_counterror_count
Usage posting idempotency (Stripe)
Implemented behavior:
- usage posting now persists to
rcm.usage_report_batchesbefore Stripe submission - each batch has:
batch_id- deterministic
stripe_idempotency_key - fixed
stripe_usage_timestamp - Stripe posts use deterministic identifiers/idempotency keys (Meter Events or usage-record fallback), so timeout-boundary retries do not double-bill
- batch status lifecycle is explicit:
pending->postedon successpending/failed-> retried by reporting worker- only unposted batches are selected for retry
Reporting endpoints
- Caller-scoped report:
GET /v1/usage(API key auth)- Plan/remaining limits for caller:
GET /v1/limits- Admin account-level report:
GET /v1/billing/accounts/{email}/usage- DB-backed aggregate; reflects flushed usage batches (eventually consistent within
USAGE_FLUSH_INTERVAL_SECONDS) - Admin account lifecycle snapshot:
GET /v1/billing/accounts/{email}/status- returns plan, subscription status, renewal date, last payment event result, and API key status summary
Self-Serve Dashboard Endpoints (Control-Plane V1)
POST /v1/control-plane/auth/signup- public email/password signup + workspace dashboard session
POST /v1/control-plane/auth/login- public email/password login + workspace dashboard session
POST /v1/control-plane/setup-links(admin token required)- creates one-time setup link for workspace onboarding
POST /v1/control-plane/setup-links/redeem- consumes setup link and returns short-lived dashboard session token
GET /v1/control-plane/workspace- returns workspace summary for current dashboard session
POST /v1/control-plane/workspace- creates/updates workspace display name for current session
- required payload:
{"workspace_name":"<name>"} GET /v1/control-plane/workspace/api-keys- lists workspace keys (metadata only, never raw key material)
POST /v1/control-plane/workspace/api-keys- issues new key and returns cleartext once
POST /v1/control-plane/workspace/api-keys/{api_key_id}/rotate- replacement issuance + old-key revoke
POST /v1/control-plane/workspace/api-keys/{api_key_id}/revoke- explicit revoke
POST /v1/control-plane/sessions/logout- revokes current dashboard session token
4. Free-Tier Gating Logic
Free tier policy (trial):
- Monthly limit: 1,000 scoring calls
- RPS limit: 1 req/sec with burst 5
- Concurrency limit: 1 in-flight scoring request per trial key
Enforcement path:
- Before scoring, API reserves one usage unit in batcher
- If projected monthly usage exceeds plan limit:
- trial: HTTP
402with structured quota payload andupgrade_url - non-trial free/admin-managed plans: HTTP
429with monthly-limit message
- If per-second request window exceeds plan RPS:
- HTTP
429withRetry-After: 1
- If trial per-key concurrency limit is exceeded:
- HTTP
429withRetry-After: 1
Applied to scoring endpoints:
POST /v1/scorePOST /v1/claims/denial/scorePOST /v1/claims/prior-auth/predictPOST /v1/claims/reimbursement/estimate
Explicitly out of billing scope:
POST /v1/feedback(feedback telemetry only, no scoring compute charge)
Additional abuse controls:
- Global scoring concurrency cap via
API_GLOBAL_SCORING_CONCURRENCY_CAP - Daily emergency shutdown threshold via
API_DAILY_EMERGENCY_REQUEST_THRESHOLD
5. Plan Change and Collection Lifecycle
Plan upgrade/downgrade behavior:
- Stripe handles billing-cycle proration according to subscription configuration
- API access scope/limits follow the latest processed subscription webhook state
- practical effect: plan/scope changes are applied when webhook processing commits
Cancellation behavior:
- on
customer.subscription.deleted, account status is set to canceled and API keys are deactivated - no end-of-period grace is applied in current runtime policy
Payment failure behavior:
- on
invoice.payment_failed, account status transitions topast_due - API keys are deactivated immediately
- protected scoring requests return HTTP
402 Payment Requireduntil account returns to active/trialing
6. Billing Observability
Token-service exports Prometheus metrics at GET /metrics with billing/webhook counters and timers:
token_service_stripe_webhook_received_total{event_type}token_service_stripe_webhook_verified_total{event_type}token_service_stripe_webhook_failed_total{stage,reason}token_service_stripe_webhook_processed_total{event_type,outcome}token_service_stripe_webhook_processing_seconds{event_type,outcome}token_service_billing_provisioning_total{event_type,result}token_service_billing_revocations_total{source,result}token_service_billing_active_subscriptions
7. Stripe Customer Portal
Current status:
- self-serve Stripe Customer Portal is not implemented in v1
- card updates, invoice requests, and plan/cancellation support are handled through support channels
Support contact:
support@sentinelsignal.io
8. Security Hardening Callouts
Webhook verification requirements:
- verify Stripe signature using the raw request body bytes and
Stripe-Signature - do not reserialize JSON before verification; raw-body integrity is required for valid signature checks
IP allowlisting note:
- IP allowlisting for Stripe webhooks is optional, not a substitute for signature verification
- Stripe IP ranges can change; if allowlisting is used, it must be continuously maintained from Stripe-published ranges
Environment Variables
Core:
API_KEY_HASH_SECRETAPI_KEY_AUTH_ENABLEDUSAGE_BATCHING_ENABLEDUSAGE_BATCH_SIZEUSAGE_FLUSH_INTERVAL_SECONDSAPI_GLOBAL_SCORING_CONCURRENCY_CAPAPI_DAILY_EMERGENCY_REQUEST_THRESHOLD
Stripe:
STRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETSTRIPE_PRICE_ID_GROWTHSTRIPE_PRICE_ID_PROSTRIPE_CHECKOUT_SUCCESS_URLSTRIPE_CHECKOUT_CANCEL_URLSTRIPE_WEBHOOK_TOLERANCE_SECONDSSTRIPE_USAGE_REPORTING_ENABLEDSTRIPE_USAGE_REPORT_INTERVAL_SECONDSSTRIPE_USAGE_REPORT_BATCH_LIMITSTRIPE_USAGE_REPORT_TIMEOUT_SECONDS
Notes
- API keys are stored as non-reversible hashes; plaintext key is returned only at issue time.
- Webhook processing is idempotent by Stripe event id.
- Batching is eventually consistent for persisted counters, but reservation checks account for pending in-memory usage.
GET /portal/specnow merges all token-service/v1/...endpoints so the portal console exposes checkout, key lifecycle, and billing usage operations./portal/dashboardnow includes self-serve Stripe Checkout forgrowth/prousing dashboard-session-auth control-plane endpoints.