Browse documentationBilling boundaries

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
PUT/v1/ws/{workspaceSlug}/billing-user

Designate 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
PUT/v1/ws/{workspaceSlug}/billing-cap

Set 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
GET/v1/ws/{workspaceSlug}/billing/usage

Read 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
GET/v1/ws/{workspaceSlug}/billing/ledger

List 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
GET/v1/user/billing

Read 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
POST/v1/user/billing/subscription/checkout

Create 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
POST/v1/user/billing/subscription/plan

Upgrade, 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