Skip to content

License each tenant

If your software has its own accounts and tenants, license each tenant rather than each user. A tenant buys seats: the license issues one seat for each, under one subscription. Your backend issues one credential per seat, keeps it against the tenant, and serves every request of that tenant with one of its seats. Your users never become Monaiq accounts and never hold a license code.

Seats are capacity. Every seat shares the license’s features, but each seat carries its own rate windows and its own allowance; nothing is pooled across a license’s seats. A tenant that needs more throughput or a larger allowance adds seats, and your backend spreads the tenant’s requests across them.

Monaiq bills for the seats and issues them. Which request runs on which seat is your decision, made in your backend.

  1. Create a checkout for the tenant. From your backend, create an embedded checkout. Set CorrelationId to your own tenant id, Quantity to the seats bought, and ReturnUrl to a route on your site. Send a different RequestId for every purchase: a repeated RequestId is the same purchase, and the same RequestId with a different Quantity is refused.
  2. Send the buyer on, now. A paid offering returns a payment URL; a free offering returns a claim link. Redirect the buyer to whichever you got, in the same browser. On the claim link they confirm the license, prove the address with a one-time code, and are sent back to ReturnUrl?session_id=.... The claim link is also emailed, as a fallback for a buyer who closed the tab, never as the path.
  3. Read the result. When the buyer lands on your ReturnUrl, read the result with the session id: it is already completed. Poll only if you set no ReturnUrl. The result names the license (LicenseId) and its seats (SeatIds). A purchase of one seat also carries that seat’s license code (EncodedCredential); for more than one seat there is none — issue one per seat yourself.
  4. Find the tenant’s license. GET /licenses?correlationId=<tenant id> finds it by your own id, and GET /licenses/{licenseId}/seats lists its seats. Every seat of one license carries the same LicenseId.
  5. Issue one credential per seat. Issue a license code for every live seat, with a label that names the seat in your own terms, and keep it in your secret store against the seat. It is shown once. Treat it as a secret: it authorizes and meters that seat.
  6. Serve each request with one of the tenant’s seats. In .NET, register your own ILicensingContextProvider with AddSidubLicensing<TContextProvider>(). It resolves the licensing context once per request scope, so it returns the context of whichever seat you choose for the request’s tenant. Choosing a seat shows the choice.

Choose the seat with the most of its allowance left. The SDK’s state view carries every seat’s allowance figures, so the choice needs no extra call: ILicenseStateProvider.GetState projects the cached authorization and the in-process counters.

using Sidub.Licensing;
using Sidub.Licensing.Client;
using Sidub.Licensing.Context;
public sealed class TenantSeatContextProvider(
ITenantResolver tenants, // yours: the tenant of the current request
ISeatStore seats, // yours: the tenant's seats and their stored credentials
ILicenseStateProvider stateProvider) : ILicensingContextProvider
{
private const string AllowanceKey = "document-allowance"; // your allowance feature key
public async Task<LicensingContext?> ResolveContextAsync(CancellationToken cancellationToken = default)
{
var tenant = await tenants.ResolveAsync(cancellationToken);
if (tenant is null) return null;
LicensingContext? best = null;
long bestRemaining = -1;
foreach (var seat in await seats.ListLiveAsync(tenant.Id, cancellationToken))
{
var context = LicensingContext.FromEncodedString(seat.EncodedCredential);
var state = await stateProvider.GetState(
LicensingServiceReference.ServiceReference, context, cancellationToken);
if (state is null) continue;
var allowance = state.Allowances.FirstOrDefault(a => a.FeatureKey == AllowanceKey);
var remaining = allowance is null ? long.MaxValue : allowance.Remaining;
if (remaining > bestRemaining)
{
best = context;
bestRemaining = remaining;
}
}
return best; // null: the tenant holds no live seat, and every licensing call refuses
}
}

Register it: builder.Services.AddSidubLicensing<TenantSeatContextProvider>();.

A seat whose allowance assertion returns false is spent until its PeriodEndUtc; the next request lands on the seat with the most left. When every seat is spent, the tenant is out of allowance until the period resets or a seat is added. A seat whose rate window is full answers the same way for the length of the window.

A license code is how a purchase made anywhere reaches the application it was bought for, and every integrating application accepts one. A tenant that bought on your storefront or in the Monaiq portal holds a code from the checkout, or issues one on any seat of the license. Let an owner of the tenant paste it into your application, then:

  1. Fetch an authorization with the pasted code. A refusal means the code is revoked, retired or not a license code at all.
  2. The authorization names the license (LicenseId) and the seat. Read the license, GET /licenses/{licenseId} — a 404 means it was issued by somebody else. The row carries the license’s Status and its features.
  3. Check the license is not already linked to another tenant. A license code proves the purchase, not who is pasting it, and any code of the license names the same LicenseId. Record which tenant linked each license, and refuse a code whose license another tenant already holds, unless your application shares purchases between tenants on purpose.
  4. List every seat of the license: GET /licenses/{licenseId}/seats.
  5. Issue one credential per live seat, labelled, and store each against the tenant.
  6. Discard the pasted code. The seats now run on license codes you issued and can revoke. The pasted code stays the buyer’s: it authorizes and meters one seat they paid for.

Create a paid checkout for the tenant with the same CorrelationId and the seats wanted, and send the buyer to the payment URL. On return, associate the new purchase as in the flow, then revoke the license codes of the seats the new purchase replaces. Nothing downstream is re-keyed: your users, connections and machines keep working, on the new seats.

Record the address that paid. That person manages the subscription, its invoices and its payment method in the Monaiq portal.

A second purchase of the same offering joins the tenant’s pool the same way.

Seats change in the Monaiq portal as well as through your backend. Sync a license on these signals; nothing runs on a timer:

  • A code is pasted. Associating the purchase is the sync.
  • You changed something. After you add or retire a seat, sync once.
  • A seat is refused. GetAuthorization throws LicenseAuthorizationDeniedException with the reason: SeatRetired, LicenseSuspended, LicenseCancelled or Expired. A code the buyer revoked is refused with LicensingApiException code CredentialRevoked. GetState returns null for either and logs why. Sync that seat’s license once and show the tenant what you found.
  • The tenant asks. Put a sync control next to the paste box. Seats added in the portal reach you this way.

A sync is:

  1. List the license’s seats by LicenseId.
  2. Issue a license code for every live seat that has none.
  3. Drop a seat whose RetiredUtc is set. Its license codes are already revoked.
  4. Read the license’s Status. Suspended, Cancelled and Paused refuse every seat of the license at its next authorization; show the tenant its billing state and the Monaiq portal link.

From your backend, add seats to a license: the new seats are charged to the customer’s card with the payment processor’s default proration and come back in the response, each with no license code yet. Retire a seat the tenant no longer needs: it is credited the same way, its license codes are revoked, and it is refused at its next authorization.

A change reaches a running seat at its next authorization refresh. The refresh interval is the authorization duration in Credentials and keys.

A machine, a build agent or a deployment the tenant runs itself holds a credential of one seat. Issue it from your backend after the operator signs in to your application, label it with the machine’s name, and revoke it to cut the machine off. A deployment the tenant runs holds the credential in its own configuration and needs no key of yours.

The management API is for your backend:

  • Create a license directly, of any number of seats, for a tenant who did not check out — a contract you invoice yourself. Set CorrelationId to the tenant id.
  • Find a tenant’s license by your own id with GET /licenses?correlationId=..., without storing Monaiq’s ids. The license carries its status and term; each seat carries LicenseStatus and RetiredUtc.
  • Add seats to a license. The new seats are charged to the customer’s card with the payment processor’s default proration.
  • Retire a seat the tenant no longer needs. It is credited the same way, its license codes are revoked, and it is refused at its next authorization.
  • Issue or revoke a license code for a seat you issued.

None of these operations is an MCP tool: licenses, seats and license codes are management API and portal work.

  • The claim needs the right address. A free checkout is claimed by the buyer signing in. The account they sign in with must be the one for the address you sent as CustomerEmail, so send the address they will actually use.
  • The buyer sees your name. The claim page and its emails name you by your Monaiq profile’s contact name, or your legal name if that is blank. With neither set they say “a seller on Monaiq”, so set one before you send anybody a claim.
  • One credential per seat per process. Two credentials of one seat in the same process share one cached authorization, so revoking one of them takes effect only when that authorization expires. Issue a second credential for a seat only for a separate machine or deployment.
  • The last seat cannot be retired. A license always keeps at least one live seat. To end the license, cancel its subscription instead.
  • A retired seat stays retired. It is listed with its RetiredUtc, keeps no license codes, and cannot be added back. Add a seat instead.
  • Seats change only on an active license. A paused, suspended or cancelled one refuses the change, and a one-time purchase keeps the seats it was bought with.
  • Monaiq’s own plans are one seat. They are refused in multiples.