Billing API
Manage subscriptions, view invoices, and track usage programmatically. All billing is powered by Stripe — checkout and portal sessions redirect to Stripe-hosted pages.
Never embed bearer tokens in browser/mobile code. All examples below assume server-side credential injection only.
Plans
| Plan | Price | Identities | API calls/mo | Domains |
|---|---|---|---|---|
starter | See pricing source | Plan-defined | Plan-defined | Plan-defined |
growth | See pricing source | Plan-defined | Plan-defined | Plan-defined |
pro | See pricing source | Plan-defined | Plan-defined | Plan-defined |
enterprise | Contract | Plan-defined | Plan-defined | Plan-defined |
AIdenID uses Stripe for all payment processing. Checkout sessions and customer portal sessions redirect to Stripe-hosted pages. Plan values can change over time; treat this table as a shape reference and use the authoritative pricing page and billing API responses for current limits.
Get subscription
Retrieve the current subscription details for the organization, including plan, status, and trial information.
curl
curl "https://api.aidenid.com/v1/billing/subscription" \
-H "Authorization: Bearer <SERVER_INJECTED_API_KEY>" \
-H "X-Org-Id: org_abc123" \
-H "X-Project-Id: proj_def456"Response 200 OK
{
"plan": "growth",
"status": "active",
"current_period_start": "2026-04-01T00:00:00Z",
"current_period_end": "2026-05-01T00:00:00Z",
"trial": {
"id": "trial_g1h2i3j4",
"status": "CONVERTED",
"started_at": "2026-03-25T10:00:00Z"
},
"billing_provider": "stripe",
"customer_reference": "provider-managed"
}Create checkout session
Create a Stripe checkout session for a new subscription or plan change. Returns a short-lived redirect token that your backend can exchange for an immediate redirect.
Treat checkout_redirect_token as single-use and short-lived. Redeem it server-side only, reject expired tokens, and deny replayed token IDs even if a client retries the request.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
plan | string | Yes | Target plan: starter, growth, pro, or enterprise |
curl
curl -X POST https://api.aidenid.com/v1/billing/checkout \
-H "Authorization: Bearer <SERVER_INJECTED_API_KEY>" \
-H "X-Org-Id: org_abc123" \
-H "X-Project-Id: proj_def456" \
-H "Idempotency-Key: <GENERATED_UNIQUE_KEY>" \
-H "Content-Type: application/json" \
-d '{ "plan": "growth" }'Python
import requests
import random
import time
class BreakerStore:
"""Process-safe breaker contract backed by Redis/DB atomic ops."""
def should_allow(self, key: str, now_s: float) -> bool:
raise NotImplementedError
def record_success(self, key: str) -> None:
raise NotImplementedError
def record_failure(self, key: str, *, open_for_s: int) -> None:
raise NotImplementedError
breaker_store = BreakerStore()
def mint_service_token_via_oidc() -> str:
"""Mint a short-lived service identity JWT via your OIDC token broker.
- Token TTL must not exceed 300 seconds.
- Audience must be scoped to the target service (e.g. "billing-gateway").
- Include a cryptographically random jti claim and never reuse a jti value.
- Bind subject/service principal claims to one backend workload identity.
- Never log, cache to disk, or embed the returned token in URLs.
"""
# Implementation depends on your identity provider (e.g. AWS IAM, GCP metadata, Vault).
raise NotImplementedError("Replace with your OIDC / workload-identity token broker call")
def _safe_error_context(response: requests.Response) -> dict:
request_id = (
response.headers.get("x-request-id")
or response.headers.get("X-Request-Id")
or "unknown"
)
retry_after = response.headers.get("retry-after")
return {
"status": response.status_code,
"request_id": request_id,
"retry_after": retry_after,
}
def raise_sanitized_http_error(*, operation: str, response: requests.Response) -> None:
ctx = _safe_error_context(response)
# Log requestId/status only. Never log Authorization or checkout_redirect_token values.
raise RuntimeError(
f"{operation}_failed status={ctx['status']} request_id={ctx['request_id']} retry_after={ctx['retry_after']}"
)
def post_with_resilience(
url: str,
*,
breaker_key: str,
headers: dict[str, str],
json: dict,
timeout: tuple[float, float],
max_attempts: int = 3,
):
# Fail closed if breaker state cannot be read/written.
now = time.time()
try:
if not breaker_store.should_allow(breaker_key, now):
raise RuntimeError("billing_upstream_circuit_open")
except Exception as exc:
raise RuntimeError("breaker_store_unavailable_fail_closed") from exc
for attempt in range(max_attempts):
try:
response = requests.post(url, headers=headers, json=json, timeout=timeout)
if response.status_code in (429, 503):
try:
breaker_store.record_failure(breaker_key, open_for_s=30)
except Exception as exc:
raise RuntimeError("breaker_store_unavailable_fail_closed") from exc
if attempt >= max_attempts - 1:
return response
delay_s = min(2 ** attempt, 8) + random.uniform(0, 0.25)
time.sleep(delay_s)
continue
try:
breaker_store.record_success(breaker_key)
except Exception as exc:
raise RuntimeError("breaker_store_unavailable_fail_closed") from exc
return response
except (requests.Timeout, requests.ConnectionError):
try:
breaker_store.record_failure(breaker_key, open_for_s=30)
except Exception as exc:
raise RuntimeError("breaker_store_unavailable_fail_closed") from exc
if attempt >= max_attempts - 1:
raise
delay_s = min(2 ** attempt, 8) + random.uniform(0, 0.25)
time.sleep(delay_s)
continue
raise RuntimeError("billing_upstream_attempts_exhausted")
resp = post_with_resilience(
"https://api.aidenid.com/v1/billing/checkout",
breaker_key="stripe:checkout:org_abc123",
headers={
"Authorization": "Bearer <SERVER_INJECTED_API_KEY>",
"X-Org-Id": "org_abc123",
"X-Project-Id": "proj_def456",
"Idempotency-Key": "<GENERATED_UNIQUE_KEY>",
},
json={"plan": "growth"},
timeout=(3.05, 10),
)
if resp.status_code >= 400:
raise_sanitized_http_error(operation="billing_checkout", response=resp)
def _redeem_checkout_from_response(checkout_response: requests.Response) -> requests.Response:
# Hand off the raw response so the redirect token is never assigned to a
# reusable variable, logged, or persisted in application state.
return post_with_resilience(
"https://internal.example.com/billing/redeem-checkout",
breaker_key="stripe:redeem_checkout:org_abc123",
headers={
# Mint a fresh short-lived service JWT inline per request.
"Authorization": f"Bearer {mint_service_token_via_oidc()}",
"X-Service-Principal": "billing-gateway",
},
json={
# Read once and redeem immediately. Do not store the redirect token.
"checkout_redirect_token": checkout_response.json()["checkout_redirect_token"],
"request_id": "req_checkout_123",
},
timeout=(3.05, 10),
)
# Never log or persist sensitive token values in variables, traces, or exception text.
# Immediately hand off to a trusted backend redirect endpoint.
# Require mutual TLS and service identity auth between edge and backend.
redeem_resp = _redeem_checkout_from_response(resp)
if redeem_resp.status_code >= 400:
raise_sanitized_http_error(operation="redeem_checkout", response=redeem_resp)
Install logger sanitizers so Authorization and checkout_redirect_token are always redacted before any log sink write, including error paths.
Do not expose checkout token redemption directly to browsers. Protect internal redemption endpoints with service identity authentication and mTLS, deny unauthenticated callers, and validate JWT iss/aud/sub/expplus single-use jti replay checks before token redemption. Log request IDs plus service principal identity for every redemption attempt.
Service JWT replay guard contract
# Pseudo-code: validate bearer JWT and enforce one-time jti usage
claims = verify_service_jwt(
token=bearer_token,
required_audience="billing-gateway",
required_issuer="https://issuer.example.com",
max_ttl_seconds=300,
)
jti = claims["jti"]
service_principal = claims["sub"]
cache_key = f"svc_jti:{service_principal}:{jti}"
was_inserted = replay_cache.set_if_absent(cache_key, value="1", ttl_seconds=claims["exp"] - now_epoch)
if not was_inserted:
raise UnauthorizedError("service_token_replayed")Server-side one-time consume contract
# Pseudo-code for POST /billing/redeem-checkout (backend only)
def redeem_checkout_token(token_id: str, now_epoch: int):
# Atomic consume in DB/Redis:
# - return status="missing" when token does not exist
# - return status="expired" when now_epoch > expires_at
# - return status="replayed" when consumed_at is already set
# - otherwise set consumed_at=now_epoch and return status="ok"
consume_result = token_store.consume_once(token_id=token_id, now_epoch=now_epoch)
if consume_result.status != "ok":
# External callers always receive the same error contract to avoid token-state oracle leaks.
logger.info(
"checkout_token_redeem_denied",
extra={
"requestId": request_id,
"denialCategory": "token_invalid",
},
)
return 403, {"error": {"code": "token_invalid"}}
# Only now call Stripe to create/use the redirect URL.
checkout_url = stripe_client.redeem_checkout(consume_result.provider_ref)
return 200, {"checkout_url": checkout_url}Keep shared application logs coarse (token_invalid only). Any detailed token-state diagnostics should be isolated to restricted security audit channels.
Response 200 OK
{
"checkout_redirect_token": "<REDACTED_SERVER_ONLY_TOKEN>",
"expires_at": "2026-04-04T10:12:00Z"
}The checkout_redirect_token is a single-use, server-only credential. Treat it like a bearer token: never log, cache, or echo the value to the client beyond the immediate redirect.
List invoices
Retrieve billing history and invoices for the organization.
curl
curl "https://api.aidenid.com/v1/billing/invoices" \
-H "Authorization: Bearer <SERVER_INJECTED_API_KEY>" \
-H "X-Org-Id: org_abc123" \
-H "X-Project-Id: proj_def456"Response 200 OK
{
"items": [
{
"id": "inv_a1b2c3",
"amount_cents": 9900,
"currency": "usd",
"status": "paid",
"period_start": "2026-03-01T00:00:00Z",
"period_end": "2026-04-01T00:00:00Z",
"pdf_download": {
"exchange_required": true,
"token_preview": "inv_dl_••••••••",
"expires_at": "2026-04-01T00:10:00Z"
},
"created_at": "2026-04-01T00:00:00Z"
}
],
"total": 1,
"limit": 20,
"offset": 0
}Invoice download tokens are one-time credentials. Serve invoice exchange and download endpoints with Cache-Control: no-store and Pragma: no-cache, redeem tokens only from trusted backend services, and deny replayed or expired token IDs.
Get usage
Get current billing period usage metrics, including identities created, API calls made, and limits for the current plan.
curl
curl "https://api.aidenid.com/v1/billing/usage" \
-H "Authorization: Bearer <SERVER_INJECTED_API_KEY>" \
-H "X-Org-Id: org_abc123" \
-H "X-Project-Id: proj_def456"TypeScript
const controller = new AbortController();
const timeoutMs = 10_000;
const timeout = setTimeout(() => controller.abort(), timeoutMs);
const recordBillingUsageMetric = (metric: {
identitiesCreated: number;
identitiesLimit: number;
apiCalls: number;
apiCallsLimit: number;
}) => {
// Send allowlisted usage fields to your secure telemetry sink.
void metric;
};
try {
const res = await fetch("https://api.aidenid.com/v1/billing/usage", {
headers: {
"Authorization": "Bearer <SERVER_INJECTED_API_KEY>",
"X-Org-Id": "org_abc123",
"X-Project-Id": "proj_def456",
},
signal: controller.signal,
});
if (!res.ok) {
throw new Error(`billing_usage_request_failed:${res.status}`);
}
const usage = await res.json();
recordBillingUsageMetric({
identitiesCreated: usage.identities_created,
identitiesLimit: usage.identities_limit,
apiCalls: usage.api_calls,
apiCallsLimit: usage.api_calls_limit,
});
} finally {
clearTimeout(timeout);
}Response 200 OK
{
"period_start": "2026-04-01T00:00:00Z",
"period_end": "2026-05-01T00:00:00Z",
"identities_created": 247,
"identities_limit": 1000,
"active_identities": 42,
"api_calls": 15832,
"api_calls_limit": 100000,
"domains_registered": 2,
"domains_limit": 5
}Customer portal
Create a Stripe customer portal session for self-service billing management. The portal allows users to update payment methods, view invoices, and cancel subscriptions.
curl
curl -X POST https://api.aidenid.com/v1/billing/portal \
-H "Authorization: Bearer <SERVER_INJECTED_API_KEY>" \
-H "X-Org-Id: org_abc123" \
-H "X-Project-Id: proj_def456" \
-H "Idempotency-Key: <GENERATED_UNIQUE_KEY>"Response 200 OK
{
"portal_redirect_token": "<REDACTED_SERVER_ONLY_TOKEN>",
"expires_at": "2026-04-04T10:12:00Z"
}The portal_redirect_token is a single-use, server-only credential. Treat it like a bearer token: never log, cache, or echo the value to the client beyond the immediate redirect.
Mutation replay and error envelope contract
Billing mutations (/v1/billing/checkout and /v1/billing/portal) require Idempotency-Key. If the same key is replayed with the same payload hash, the API returns the original response body and status without creating a second Stripe session.
Auth and redeem errors must return the canonical envelope with requestId:
{
"error": {
"code": "token_expired",
"message": "Checkout redirect token has expired.",
"details": {
"requestId": "req_billing_123"
}
},
"requestId": "req_billing_123",
"timestamp": "2026-04-06T21:33:45.225313+00:00",
"path": "/v1/billing/redeem-checkout",
"retryable": false
}| Code | Status | When returned | Retryable |
|---|---|---|---|
missing_idempotency_key | 400 | Mutation call omitted required idempotency header | No |
unauthorized | 401 | Missing or invalid bearer credential | No |
forbidden | 403 | Credential lacks required billing scope | No |
token_replayed | 409 | Redirect token already redeemed by trusted backend | No |
token_expired | 410 | Redirect token expired before redemption | No |
dependency_timeout | 503 | Transient Stripe/upstream timeout while creating session | Yes |
Error codes
| Code | Status | Description |
|---|---|---|
no_active_subscription | 404 | Organization does not have an active subscription |
invalid_plan | 400 | Plan must be one of: starter, growth, pro, enterprise |
checkout_failed | 502 | Failed to create Stripe checkout session |
portal_failed | 502 | Failed to create Stripe portal session |
missing_idempotency_key | 400 | Mutation request missing Idempotency-Key header |
Related
- Pricing — compare plan features and limits
- Trial API — start a free trial before committing
- Domains API — domain limits vary by plan