Skip to content

Multi-Tenancy & Connected Businesses

This is the conceptual home for how Invora isolates tenants and how a parent organization manages businesses underneath it. It covers the three deployment models, tenant isolation, the org hierarchy, how capabilities and scopes gate operations, parent-rolled-up billing, the suspend/reactivate lifecycle, and the x-zitadel-orgid header for acting on another org.

The full ConnectedBusinessService RPC reference (every endpoint, field, and pagination detail) lives in the Identity API Guide. This page is the why and the model; that page is the how.

All examples use REST over gRPC-JSON transcoding (camelCase JSON fields). Base URLs: production https://gateway.invora.app, staging https://stg-gateway.invora.app. Every request carries Authorization: Bearer $TOKEN — see Authentication for obtaining $TOKEN.

Deployment Models

Invora supports three ways to structure an account. They differ in how you arrange organizations, not in which APIs exist.

Model Who uses it Structure API surface
Standalone A single company invoicing for itself One Business org Invoicing + Billing
Platform A group managing multiple entities (franchise, holding company, aggregator) One Platform org + Connected Business children + Connected Business management
Reseller A company white-labelling Invora for its own customers One Platform org + one Connected Business per customer + Connected Business management

Platform and Reseller share the same mechanism — both create child tenants through ConnectedBusinessService. The difference is commercial: a Platform manages entities it owns, a Reseller onboards external customers.

Organization types

Every organization carries an OrgType that reflects its place in the hierarchy:

OrgType Meaning
ORG_TYPE_BUSINESS A standalone company. May later apply to become a Platform.
ORG_TYPE_PLATFORM A company that manages other businesses underneath it.
ORG_TYPE_CONNECTED_BUSINESS A sub-business created and managed by a Platform.

Becoming a Platform requires an approved platform-enrollment application. See the Identity API Guide for the enrollment flow.

Tenant Isolation

Every request operates within exactly one tenant (a Zitadel organization). The tenant is resolved from the access token's home organization — the Zitadel resourceowner claim — unless the request re-targets another org with x-zitadel-orgid.

Within a tenant, all data is fully isolated:

  • Documents, parties, settings, branches, and regulation state belong to one tenant and are never visible to another through the standard APIs.
  • There are no cross-tenant reads or writes on the public invoicing, parties, or settings services.
  • A Platform parent can manage the lifecycle of its Connected Businesses (create, suspend, reactivate, delete) but cannot read their documents or parties through the standard APIs. Cross-tenant data visibility is an admin-only capability handled by Invora staff.

Tenant hierarchy

flowchart TD
  P["Platform<br/>(parent org · ORG_TYPE_PLATFORM)"]
  A["Connected Business A<br/>(child tenant)"]
  B["Connected Business B<br/>(child tenant)"]
  C["Connected Business C<br/>(child tenant)"]
  P --> A
  P --> B
  P --> C

Each Connected Business:

  • Has its own tenant (its own Zitadel org ID, surfaced as tenantId).
  • Receives its own OIDC client credentials (clientId + clientSecret) for machine-to-machine access.
  • Owns its own documents, parties, branches, and settings, fully isolated from siblings and from the parent.
  • Bills through the parent — usage rolls up to the parent's subscription (see Billing model).

Capability & Scope Gating

Two independent layers decide whether an operation is allowed: the capabilities the tenant's plan includes, and the scope the access token carries. A request must clear both. A failure on either returns PERMISSION_DENIED (gRPC code 7, HTTP 403).

Capabilities

A business declares which Invora capabilities it wants at registration (BusinessCapability), and its subscription plan grants the corresponding entitlements:

Capability Enables
BUSINESS_CAPABILITY_EINVOICING Create, validate, and submit electronic invoices (ZATCA, Peppol, ETA, etc.).
BUSINESS_CAPABILITY_BILLING Subscription management, usage metering, recurring invoicing, payment collection.

Some operations additionally require a plan-level entitlement. If the tenant's plan does not include it, the call is rejected with PERMISSION_DENIED:

Operation Required entitlement If missing
CreateConnectedBusiness connected_business PERMISSION_DENIED
Billing APIs (plans, subscriptions, wallets, …) billing PERMISSION_DENIED
Regulation / ZATCA onboarding regulations PERMISSION_DENIED

The connected_business gate is a billing entitlement, not a numeric quota: if the plan does not include the capability, CreateConnectedBusiness fails with PERMISSION_DENIED. There is no per-count RESOURCE_EXHAUSTED limit on this gate. Upgrade to a plan tier that includes the capability, then retry.

For how entitlements are attached to plans and queried per subscription, see Billing. For the full status-code catalogue and error body shape, see Error Handling.

Scopes

Beyond capabilities, every RPC is guarded by a scope that the token must hold. Connected Business operations use the Invora.Identity.v2.ConnectedBusiness.* family (for example Invora.Identity.v2.ConnectedBusiness.Create, .Suspend, .List). A token without the required scope is rejected with PERMISSION_DENIED, independent of the plan's capabilities. Scopes are issued per Authentication.

A capability-gated call resolves like this:

flowchart TD
  R["Request → ConnectedBusinessService"]
  S{"Token holds<br/>required scope?"}
  C{"Plan includes<br/>required capability?"}
  OK["Proceed"]
  D1["PERMISSION_DENIED"]
  D2["PERMISSION_DENIED"]
  R --> S
  S -- no --> D1
  S -- yes --> C
  C -- no --> D2
  C -- yes --> OK

Creating a Connected Business

A Platform creates a child tenant with name and adminEmail. The country, tax ID, and other profile details are configured later by the Connected Business itself through its own settings — they are not part of the create request.

curl -X POST https://gateway.invora.app/api/v2/identity/connected-businesses \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Riyadh Branch",
    "adminEmail": "admin@acme-riyadh.com"
  }'
Response
{
  "connectedBusiness": {
    "tenantId": "317842111002338820",
    "name": "Acme Riyadh Branch",
    "parentTenantId": "317842069254438913",
    "status": "CONNECTED_BUSINESS_STATUS_PROVISIONING"
  },
  "clientId": "317842111002338821@acme-riyadh",
  "clientSecret": "shown-once-store-securely"
}

The response carries the new tenant's status as a ConnectedBusinessStatus enum — there is no active boolean. A freshly created business starts as CONNECTED_BUSINESS_STATUS_PROVISIONING and transitions to CONNECTED_BUSINESS_STATUS_ACTIVE once provisioning completes.

Warning

clientSecret is returned only once, at creation. Store it in a secure vault immediately — it cannot be retrieved again. If lost, rotate the credentials (see the Identity API Guide).

Status lifecycle

stateDiagram-v2
  [*] --> PROVISIONING: CreateConnectedBusiness
  PROVISIONING --> ACTIVE: provisioning complete
  ACTIVE --> SUSPENDED: SuspendConnectedBusiness
  SUSPENDED --> ACTIVE: ReactivateConnectedBusiness
  ACTIVE --> DESTROYING: DeleteConnectedBusiness
  SUSPENDED --> DESTROYING: DeleteConnectedBusiness
  DESTROYING --> [*]

ConnectedBusinessStatus values: CONNECTED_BUSINESS_STATUS_PROVISIONING, _ACTIVE, _SUSPENDED, _DESTROYING (plus _UNSPECIFIED).

Billing Model (Rolled Up to the Parent)

Connected Businesses do not carry their own subscription, wallet, or payment methods. Billing is parent-scoped:

  • The Platform subscribes to a plan (for example "Platform Pro — up to 50 connected businesses").
  • Each Connected Business creates documents and consumes metered features under the parent's subscription.
  • Usage from all Connected Businesses aggregates into the parent's billing period; the parent receives one bill.
  • The Platform can review per-tenant cost attribution through the Invora admin usage-stats surface (contact Invora support).

This rollup is what makes the Platform and Reseller models work commercially: a reseller pays Invora once and bills its own customers independently. For how plans, metering, and entitlements are configured, see Billing. For the regulatory shape of charging an intermediary fee alongside the underlying supply, see Marketplace & Intermediary Invoicing.

Suspend & Reactivate

Suspending a Connected Business revokes its users' and credentials' access immediately, pauses its billing, and preserves all of its data. Provide a reason for the audit trail.

curl -X POST https://gateway.invora.app/api/v2/identity/connected-businesses/317842111002338820/suspend \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Non-payment"}'
Response
{
  "connectedBusiness": {
    "tenantId": "317842111002338820",
    "name": "Acme Riyadh Branch",
    "parentTenantId": "317842069254438913",
    "status": "CONNECTED_BUSINESS_STATUS_SUSPENDED"
  }
}

Reactivating restores access and resumes billing:

curl -X POST https://gateway.invora.app/api/v2/identity/connected-businesses/317842111002338820/reactivate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json"
Response
{
  "connectedBusiness": {
    "tenantId": "317842111002338820",
    "name": "Acme Riyadh Branch",
    "parentTenantId": "317842069254438913",
    "status": "CONNECTED_BUSINESS_STATUS_ACTIVE"
  }
}

Suspension is reversible. To remove a business permanently, use delete instead (irreversible — see the Identity API Guide).

Acting on Another Org (x-zitadel-orgid)

By default a request operates on the caller's home org. To operate on a different org within the same request, send the target tenant in the x-zitadel-orgid header:

curl -X POST https://gateway.invora.app/api/v2/documents/list \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-zitadel-orgid: 317842111002338820" \
  -H "Content-Type: application/json" \
  -d '{}'

The platform validates x-zitadel-orgid against the grants in the caller's token — no extra API call. If the caller holds a grant (role) on the target org, the request acts as that org; otherwise it is rejected. Omitting the header (or setting it to the caller's own org) operates on the home org as usual. A token cannot reach an org it has no grant on, so this is not impersonation of arbitrary tenants.

Invora staff use the same header for cross-tenant administration, backed by IAM-level grants. Those operations are out of scope for public integrators.