Resource guide
Billing belongs to the payer
Workspace usage can be visible to members, while payment methods, automatic top-ups, and personal limits and the subscription belong to the designated billing user. A workspace owner may designate another owner as the payer; that does not let the first owner manage the second person's billing.
The payer must remain a live workspace owner. Reassignment, role demotion, and removal are checked inside the same durable mutation, so concurrent administration cannot leave a workspace with an ineligible payer. The audit log records each sensitive transition.
{
"autoTopupEnabled": true,
"autoTopupThresholdMicroUsd": 5000000,
"autoTopupAmountMicroUsd": 20000000,
"maxMonthlySpendMicroUsd": 100000000
}Keep units explicit
Contract amounts use integer micro-US dollars. Convert only at the display boundary and keep the integer value through calculations so rounding cannot create or erase spend.
Handle payment blocks as ownership flows
A payment-required response should send the responsible payer to billing settings. Retrying with a different workspace key does not repair an inactive subscription, depleted balance, or monthly cap.
Keep card data on Stripe
Duet creates Stripe-hosted Checkout and Customer Portal sessions. Card entry and payment-method management therefore stay on Stripe's origin; Duet stores only the customer reference needed for webhooks and a readiness timestamp written only after Stripe verifies a successful setup or payment. A canceled Checkout never enables the portal or automatic top-ups. Successful top-ups still become credits through the idempotent webhook-to-ledger path.
Subscription Checkout accepts a catalog plan name, never a Stripe price reference. Duet resolves the environment-owned price at the server boundary. Upgrades invoice immediately, while downgrades and cancellation take effect at the paid-through boundary. In every case, a verified paid invoice is the only authority that grants a new monthly credit bucket; requesting a plan change never writes credits directly.
The Customer Portal is intentionally narrower than the first-party subscription controls. It owns payment-method updates and invoice history, while Duet owns plan changes so every tier follows the same payer authorization and catalog-price rules.
The /v1/user/billing/* routes require Duet's short-lived same-origin session credential, not an
ordinary API key. This is deliberate: workspace usage is an API resource, while a person's card and
limits are account controls that the web proxy invokes without exposing its credential.
Billing
7 operations /v1 /ws /{workspaceSlug} /billing-userDesignate a current live owner as the workspace payer.
- Scope
ws:{workspaceSlug}:admin- Request
Request JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "userId": { "type": "string", "minLength": 1 } }, "required": [ "userId" ] }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "billingUserId": { "type": "string" }, "member": { "type": "object", "properties": { "id": { "type": "string" }, "email": { "type": "string", "format": "email", "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" }, "name": { "type": "string" }, "role": { "type": "string", "enum": [ "owner", "editor", "viewer" ] }, "isBillingUser": { "type": "boolean" } }, "required": [ "id", "email", "role", "isBillingUser" ], "additionalProperties": false } }, "required": [ "billingUserId", "member" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /ws /{workspaceSlug} /billing-capSet or clear the workspace monthly spend cap.
- Scope
ws:{workspaceSlug}:admin- Request
Request JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "monthlyCapMicroUsd": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, { "type": "null" } ] } }, "required": [ "monthlyCapMicroUsd" ] }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "monthlyCapMicroUsd": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, { "type": "null" } ] } }, "required": [ "monthlyCapMicroUsd" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /ws /{workspaceSlug} /billing /usageRead workspace usage grouped over a reporting period.
- Scope
workspace_member- Request
- No JSON request body
- Query
Query parameters JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "period": { "default": "monthly", "type": "string", "enum": [ "daily", "weekly", "monthly" ] }, "group": { "default": "workspace", "type": "string", "enum": [ "workspace", "user", "app" ] }, "start": { "type": "string" }, "end": { "type": "string" } } }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "period": { "type": "string", "enum": [ "daily", "weekly", "monthly" ] }, "group": { "type": "string", "enum": [ "workspace", "user", "app" ] }, "rows": { "type": "array", "items": { "type": "object", "properties": { "workspaceId": { "type": "string" }, "period": { "type": "string" }, "spendMicroUsd": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "userId": { "type": "string" }, "appId": { "type": "string" }, "appSlug": { "type": "string" }, "inputTokens": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "outputTokens": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 } }, "required": [ "workspaceId", "period", "spendMicroUsd" ], "additionalProperties": false } } }, "required": [ "period", "group", "rows" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /ws /{workspaceSlug} /billing /ledgerList the workspace credit ledger newest-first with actor attribution.
- Scope
workspace_member- Request
- No JSON request body
- Query
Query parameters JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "limit": { "default": 100, "type": "integer", "minimum": 1, "maximum": 500 } } }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "entries": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "amountMicroUsd": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "type": { "type": "string", "enum": [ "grant", "topup", "usage", "adjustment", "reservation", "subscription_grant", "subscription_expiry" ] }, "actorUserId": { "type": "string" }, "tier": { "type": "string", "enum": [ "frontier", "balanced", "economy" ] }, "createdAt": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 } }, "required": [ "id", "amountMicroUsd", "type", "createdAt" ], "additionalProperties": false } } }, "required": [ "entries" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /user /billingRead the payer account and every workspace it funds. Answers a user API key as well as the dashboard.
- Scope
api_key- Request
- No JSON request body
- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "autoTopupEnabled": { "type": "boolean" }, "autoTopupThresholdMicroUsd": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "autoTopupAmountMicroUsd": { "type": "integer", "minimum": 5000000, "maximum": 9007199254740991 }, "maxMonthlySpendMicroUsd": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "monthlySpendMicroUsd": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "monthlySpendPeriodKey": { "type": "string" }, "balanceMicroUsd": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "lastTopupError": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "at": { "type": "number" } }, "required": [ "code", "message", "at" ], "additionalProperties": false }, "consecutiveTopupFailures": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "firstPaidAt": { "type": "number" }, "paidCreditBalanceMicroUsd": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "monthlyCreditBalanceMicroUsd": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "subscription": { "anyOf": [ { "type": "object", "properties": { "plan": { "type": "string", "enum": [ "starter", "pro", "max" ] }, "status": { "type": "string", "enum": [ "incomplete", "incomplete_expired", "trialing", "active", "past_due", "canceled", "unpaid", "paused" ] }, "isActive": { "type": "boolean" }, "currentPeriodStart": { "type": "number" }, "currentPeriodEnd": { "type": "number" }, "creditsExpireAt": { "type": "number" }, "cancelAtPeriodEnd": { "type": "boolean" }, "pendingPlan": { "type": "string", "enum": [ "starter", "pro", "max" ] } }, "required": [ "plan", "status", "isActive", "currentPeriodStart", "currentPeriodEnd", "creditsExpireAt", "cancelAtPeriodEnd" ], "additionalProperties": false }, { "type": "null" } ] }, "trial": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "available", "exhausted", "ended" ] } }, "required": [ "status" ], "additionalProperties": false }, "lowCreditWarning": { "anyOf": [ { "type": "object", "properties": { "periodStart": { "type": "number" } }, "required": [ "periodStart" ], "additionalProperties": false }, { "type": "null" } ] }, "hasPaymentMethod": { "type": "boolean" }, "workspaces": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "slug": { "type": "string" }, "name": { "type": "string" }, "storage": { "type": "object", "properties": { "usableGb": { "type": "number" }, "usedBytes": { "type": "number" }, "sampledAt": { "type": "number" }, "plan": { "type": "string", "enum": [ "starter", "pro", "max" ] }, "nextPlan": { "type": "string", "enum": [ "starter", "pro", "max" ] }, "nextUsableGb": { "type": "number" } }, "required": [ "usableGb", "plan" ], "additionalProperties": false } }, "required": [ "id", "slug", "name", "storage" ], "additionalProperties": false } } }, "required": [ "monthlySpendMicroUsd", "balanceMicroUsd", "consecutiveTopupFailures", "paidCreditBalanceMicroUsd", "monthlyCreditBalanceMicroUsd", "subscription", "trial", "lowCreditWarning", "hasPaymentMethod", "workspaces" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /user /billing /subscription /checkoutCreate a Stripe-hosted Checkout session for a server-resolved subscription plan. Answers a user API key.
- Scope
api_key- Request
Request JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "workspaceSlug": { "type": "string" }, "plan": { "type": "string", "enum": [ "starter", "pro", "max" ] } }, "required": [ "workspaceSlug", "plan" ] }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "url": { "type": "string", "format": "uri" }, "sessionId": { "type": "string" } }, "required": [ "url" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /user /billing /subscription /planUpgrade, schedule a downgrade, or cancel the payer subscription. Answers a user API key.
- Scope
api_key- Request
Request JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ { "type": "object", "properties": { "workspaceSlug": { "type": "string" }, "plan": { "type": "string", "enum": [ "starter", "pro", "max" ] } }, "required": [ "workspaceSlug", "plan" ] }, { "type": "object", "properties": { "workspaceSlug": { "type": "string" }, "cancel": { "type": "boolean", "const": true } }, "required": [ "workspaceSlug", "cancel" ] }, { "type": "object", "properties": { "workspaceSlug": { "type": "string" }, "resume": { "type": "boolean", "const": true } }, "required": [ "workspaceSlug", "resume" ] } ] }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "ok": { "type": "boolean", "const": true } }, "required": [ "ok" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Declared idempotent