Keyring

Billing

The published plan ladder, and what is in force for this workspace.

Generated from packages/api/src/resources/billing.controller.ts by packages/docs/tools/generate-reference.mjs. Every example is a real exchange recorded against the control plane by packages/api/src/testing/reference-examples.spec.ts, with ids from that run. Credentials in the formats this product issues (kr_, krsk_, krses_, JWTs) and values under credential-named fields (password, *_secret, *_token, *_key, lookup_hash) are elided at record time; the recorded file and every page of this site are swept for credential shapes (kr_, krsk_, krses_, JWTs, URLs carrying a password, password fields) before either is accepted. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.

What a workspace may read about its billing, and nothing else.

Both endpoints are GET and neither writes anything, which is the whole of this controller: the plan ladder is data somebody else seeds, and the subscription is written by POST /v1/workspaces and by the Stripe webhook (billing/stripe-webhook.service.ts), never here. The writes on this prefix live beside it in billing/checkout.controller.ts and billing/spend-control.controller.ts -- separate controllers so that "nothing in this file changes anything" stays a property of this file. Nothing here enforces a limit -- the one enforcement this product has is plan-limit.ts, at tenant creation.

The two differ on the test-mode boundary because their subjects do. The catalog is a published price list with no workspace in it at all, so a krsk_test_ credential reads it like any other. The subscription is this workspace's commercial terms and carries the live billing figure (active_tenants_this_month comes from keyring.billable_tenant_month, which is defined where env = 'live'), so it is refused outright, exactly as GET /v1/usage/billing is and for the same written reason.

GET /v1/billing/plans

The ladder, from keyring_meta -- a global catalog with no workspace to be scoped to, which is why it lives there and why this read is unscoped rather than inWorkspace. There is nothing workspace-shaped to filter on, and a workspace scope would suggest there were.

Ordered by rank, never by price: an "upgrade" is the ladder's own order, and comparing prices to decide direction breaks the first time a founding price or an annual interval exists. A plan that is not is_public is a row this endpoint does not serve; a retired one likewise.

Answers 200 on success.

Authentication.

  • A krsk_ secret key or a krses_ dashboard session, as Authorization: Bearer.

Example.

Request
curl https://api.keyringdev.com/v1/billing/plans \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY"
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "object": "list",
  "data": [
    {
      "object": "plan",
      "code": "free",
      "name": "Free",
      "rank": 1,
      "included_tenants": 3,
      "fair_use_requests_per_month": 150000,
      "is_self_serve": true,
      "prices": [
        {
          "object": "plan_price",
          "currency": "EUR",
          "interval": "month",
          "amount_cents": 0,
          "overage_cents_per_tenant": null
        }
      ]
    },
    {
      "object": "plan",
      "code": "team",
      "name": "Team",
      "rank": 2,
      "included_tenants": 25,
      "fair_use_requests_per_month": 5000000,
      "is_self_serve": true,
      "prices": [
        {
          "object": "plan_price",
          "currency": "EUR",
          "interval": "month",
          "amount_cents": 4900,
          "overage_cents_per_tenant": null
        }
      ]
    },
    {
      "object": "plan",
      "code": "business",
      "name": "Business",
      "rank": 3,
      "included_tenants": 150,
      "fair_use_requests_per_month": 30000000,
      "is_self_serve": true,
      "prices": [
        {
          "object": "plan_price",
          "currency": "EUR",
          "interval": "month",
          "amount_cents": 19900,
          "overage_cents_per_tenant": null
        }
      ]
    },
    {
      "object": "plan",
      "code": "scale",
      "name": "Scale",
      "rank": 4,
      "included_tenants": 750,
      "fair_use_requests_per_month": 150000000,
      "is_self_serve": true,
      "prices": [
        {
          "object": "plan_price",
          "currency": "EUR",
          "interval": "month",
          "amount_cents": 49900,
          "overage_cents_per_tenant": null
        }
      ]
    },
    {
      "object": "plan",
      "code": "enterprise",
      "name": "Enterprise",
      "rank": 5,
      "included_tenants": null,
      "fair_use_requests_per_month": null,
      "is_self_serve": false,
      "prices": []
    }
  ]
}

GET /v1/billing/subscription

What is in force for the calling workspace, read from keyring.workspace_entitlement -- which joins billable_tenant_month, the view, so the entitlement and the invoice read the one definition of the billing number rather than two.

A 404 when there is no row, and that is deliberate rather than a fallback: a workspace and its subscription are written in one transaction and every older workspace was backfilled by migration 0016, so absence is a bug of ours. Answering it with an invented free entitlement would hide that, and would be a default answer to a question that has not been asked.

Answers 200 on success.

Authentication.

  • A krsk_ secret key or a krses_ dashboard session, as Authorization: Bearer.

Example.

Request
curl https://api.keyringdev.com/v1/billing/subscription \
  -H "Authorization: Bearer $KEYRING_SECRET_KEY"
Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "object": "subscription",
  "plan_code": "free",
  "included_tenants": 3,
  "overage_cents_per_tenant": null,
  "currency": "EUR",
  "amount_cents": 0,
  "interval": "month",
  "status": "none",
  "collection_state": "current",
  "grace_until": null,
  "active_tenants_this_month": 1,
  "budget_cap_cents": 0,
  "effective_tenant_ceiling": 3,
  "tenants_held": 1,
  "payments_live": false
}

On this page