Billing guide

Getting started

Getting started with the local Billing API

This Rails API is a merchant-scoped local implementation. It validates requests and responses against the checked-in Frisbii Transform OpenAPI snapshot, but it is not a live Frisbii proxy and does not claim complete semantics for all 159 source operations.

1. Obtain a client identity

An operator creates an active API client for one merchant and grants billing.read, billing.write, or both. API clients and admin browser sessions are separate principals.

2. Request an access token

The local token endpoint implements the OAuth client_credentials grant:

curl -sS -X POST 'https://billing.example.invalid/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'client_id=YOUR_CLIENT_ID' \
  --data-urlencode 'client_secret=YOUR_CLIENT_SECRET'

In local API mode, a successful response contains access_token, token_type: bearer and expires_in: 3600, with Cache-Control: no-store. In Keycloak API mode, the same local endpoint restricts requested scopes to the active local grant and uses the configured Keycloak client-credentials endpoint. The vendored Transform snapshot mentions a vendor token URL but does not define its request or response schema, so this local contract is not a Frisbii OAuth-parity claim.

3. Call a read operation

curl -sS \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  'https://billing.example.invalid/api/v1/customers?skip=0&take=20'

Ordinary GET requests require billing.read. The self-service-token GET creates a bearer capability and therefore requires billing.write. Other mutations require billing.write.

Use query parameters only where the exact OpenAPI operation declares them. Customer, contract and order lists currently default take to 200 and cap it at 500. Catalog families use their own validated bounds.

4. Make a safe write

JSON fields retain their OpenAPI PascalCase names. Send Content-Type: application/json and a stable, printable Idempotency-Key of 1 to 128 ASCII characters for each logical mutation:

curl -sS -X POST 'https://billing.example.invalid/api/v1/customers' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: customer-example-001' \
  --data '{
    "ExternalCustomerId": "crm-example-001",
    "CompanyName": "Example Customer GmbH",
    "EmailAddress": "billing@example.invalid"
  }'

Replaying the same actor, method, path, body and query with the same key returns the saved result. Reusing the key with different content returns 409 IdempotencyConflict.

The dispatcher enforces a quoted If-Match ETag for PUT and PATCH operations whose OpenAPI path parameter is literally {id}, and optionally for the exact POST /api/v1/emails/{id}/resend operation. A changed lock version returns 412 PreconditionFailed. The resend guard locks the email through mutation; a matching idempotent replay returns the saved result before checking the changed version. Calls without If-Match retain their existing behavior. Do not assume the guard applies to other POSTs, collection mutations or paths using a different parameter name such as {contractId} or {orderId}.

API request bodies are limited to 1 MiB, raw query strings to 16 KiB, and authorization input to 16 KiB. A body must be JSON when present. Cross-merchant identifiers return 404.

5. Discover current support

validated_response_exercised means a named local test completed response wiring for that exact method and path. It does not establish full endpoint semantics, Frisbii vendor parity, provider behavior or production readiness.

Creating a contract from a customer

Open Kunden, select the customer, then choose Vertrag hinzufügen. Choose a plan variant, price list and start date, and calculate the preview. The price-list label states Netto or Brutto; the preview separates net, VAT and gross amounts. A blank price-list choice uses the merchant’s setting.

After checking the preview, Vertrag erstellen creates and commits the order. If payment confirmation is required or completion fails, the existing order stays available for recovery. Bei Vertragsbeginn abrechnen is an explicit option; invoice email delivery remains a separate configured workflow.

The standalone Neue Bestellung form creates an order only. Its result page now exposes the separate contract-completion action and current totals/status.