Skip to content

HTTP API

This page is for developers who call Monaiq without an SDK, or need to know exactly what a call returned. It documents the HTTP surface an integrator can call directly. The SDKs make these calls for you, so most integrations never need it, until a request fails and you want to know exactly what came back. Base URLs are in Endpoints.

There are two credentials and two sets of paths, and each credential reaches only its own.

The account API key rides in the dub-apiKey header. It is your server-side identity as a seller, and it reaches the checkout operations and the management surfaces:

POST /licensing/checkout/embedded HTTP/1.1
Host: api.monaiq.com
Content-Type: application/json
dub-apiKey: <your API key>

A runtime token (the RuntimeToken inside a SIDUB_LIC_ credential) rides as a bearer and reaches the runtime paths under /licensing/runtime and /consumption/runtime. Those are the paths a distributed application calls:

POST /licensing/runtime/GenerateLicenseAuthorization HTTP/1.1
Host: api.monaiq.com
Content-Type: application/json
Authorization: Bearer <RuntimeToken>

The runtime paths take no dub-apiKey, no Ocp-Apim-Subscription-Key and no subscription at all; the gateway strips both key headers before the request reaches the backend. A runtime credential carries no account authority, and the account key reaches no runtime path.

Headers are the one documented way to present either value. Never put a credential in a URL: URLs land in access logs, proxies and browser history. Where each comes from, and the encoded SIDUB_LIC_ credential the SDKs use, are covered in Credentials and keys.

Every response carries an X-Correlation-Id header. Send your own x-correlation-id request header and the platform adopts your value, so a failing call stitches into your own traces; otherwise one is generated. The same value appears in the error body below; quote it when you raise a support request.

Every endpoint returns one JSON shape on failure. This is the product-wide contract across the platform’s HTTP surface:

{
"Code": "ValidationError",
"Message": "RequestId, OfferingId, CorrelationId, and CustomerEmail are required.",
"CorrelationId": "0f8fad5bd9cb469fa16570867728950e",
"Details": [
{ "Code": "ValidationError", "Message": "CustomerEmail is required.", "Field": "CustomerEmail" }
]
}
  • Code is machine-readable and PascalCase. Branch on it, never on Message.
  • Message is for humans; its wording may change without notice.
  • CorrelationId repeats the X-Correlation-Id header value.
  • Details is optional: per-field entries on validation failures, the denial reason on AuthorizationDenied, and the period end on AllowanceExhausted.
Code HTTP status Meaning
ApiKeyMissing 401 No dub-apiKey header on the request.
ApiKeyInvalid 403 The key does not resolve to an account.
LicenseNotActive 409 The license is paused, suspended or cancelled, so its seats cannot move.
SeatsNotSupported 409 This license’s seats cannot be changed by this call: a Monaiq plan, a plan sold one seat at a time, or a one-time purchase. On a one-time license the Message reads that the buyer adds seats to a one-time license from their Monaiq license page, because your server cannot pay for them.
InvalidFilter 400 A filter the route needs is missing, or one it was given is not a shape it serves, is one of two sent at once, or is not a valid value.
InvalidPaging 400 top is not a whole number from 1 to 500, or continuationToken is not one this list issued, was issued for other filters, or no longer points at a row. Restart without the token. See Paging.
LastSeat 409 The seat is the license’s last live seat. Cancel the license instead of retiring it.
SeatRetired 409 The seat is retired, so no license code is issued for it.
InvalidState 409 The lifecycle change does not fit the license’s state: a cancelled license is neither suspended nor resumed.
InvalidReason 400 A suspend or cancel named a Reason that is not one of that route’s codes. Message lists them.
NoteRequired 400 A suspend or cancel gave Other as its Reason with no Note. Nothing was changed.
InvalidRefund 400 A cancel named a Refund that is not one of that route’s words. Message lists them.
RefundNotOffered 409 The license no longer offers the Refund the cancel asked for. Message says why; nothing was changed.
NotRefundable 409 A seat’s cancel asked for a refund the license cannot give: no payment holds the seat’s price, or the processor credits it instead. Nothing was retired.
ConcurrencyConflict 409 Another request changed the license or seat while this one was being made. Read it again and retry; repeating the request is safe.
SubscriptionUpdateFailed 502 The payment processor did not change the subscription, so nothing was changed.
CredentialSurfaceUnavailable 503 Runtime credentials cannot be issued right now. Retry, and contact support if it persists.
RuntimeTokenMissing 401 A runtime path was called with no bearer.
RuntimeTokenInvalid 401 The bearer did not validate: signature, issuer, audience or scope.
CredentialUnknown 403 The token is genuine, but the credential it names is not known to this license issuer.
CredentialRevoked 403 The credential was revoked. Issue a new one.
IssuerMismatch 403 The body asserts an issuer that is not the key’s account.
AuthorizationDenied 403 The seat exists but authorization is refused. Details[0].Code is the reason: Expired, LicenseSuspended, LicenseCancelled, SeatRetired or ProductMismatch (the credential was not issued for the seller or product the application declares).
AllowanceExhausted 403 The seat’s allowance has no units left to grant in the current billing period. Details[1].Message is when the period resets.
FeatureUnknown 404 The license has no allowance feature with that key.
NotFound 404 Nothing here for that identifier.
InvalidPayload 400 The request body could not be read at all.
PayloadTooLarge 413 The request body is larger than the route reads: 64 KB on the license, checkout and runtime routes, 4 KB on the seat, credential and lifecycle routes. Nothing is changed.
ValidationError 400 The body was read but a field fails validation; see Details.
CheckoutFailed 400 The checkout could not proceed; Message states why.
RequestIdInUse 409 The RequestId was already used for something else: a checkout by another buyer or seller, a seat add on this license with a different Count, or a different allowance adjustment of the seat.
SeatRetired 409 The seat is retired, so its allowance cannot be adjusted.
RequestInProgress 409 An earlier call with the same RequestId is still completing. Repeat the call in a moment and it answers what that call did.
LedgerBusy 429 The seat’s allowance ledger was written by other runtimes on every attempt. Retry the same request, with the same RequestId, after Retry-After; the SDKs do.
StoreUnavailable 503 The platform’s store is still connecting. Nothing was changed; retry after Retry-After (5 seconds). The SDKs do.
ServerError 500 An unexpected platform failure. Retry later, and quote the correlation id if it persists.

A bearer the gateway itself refuses never reaches the backend, so it answers with the gateway’s own 401 and a WWW-Authenticate: Bearer realm="monaiq-runtime", scope="license.runtime" header rather than an ApiError body.

POST /licensing/checkout/embedded creates a checkout session so a customer can buy an offering without leaving your application. The seller-side story is in Deliver to your customers; this is the wire contract underneath it.

{
"RequestId": "5f2d0c44-0000-0000-0000-000000000000",
"OfferingId": "b3b1f6e0-0000-0000-0000-000000000000",
"CorrelationId": "order-8842",
"CustomerEmail": "buyer@example.com",
"SuccessUrl": "https://app.example.com/thanks",
"CancelUrl": "https://app.example.com/pricing",
"Quantity": 25,
"ReturnUrl": "https://app.example.com/team"
}
Field Required or optional Meaning
OfferingId Required The offering being bought.
CorrelationId Required Your own reference for this checkout, up to 128 characters. It comes back with the completed result so you can match the sale to your side.
CustomerEmail Required The address the offer is for. It is validated for shape and never looked up, so an address with a Monaiq account and one without do identical work and return the same shape. Nothing is provisioned for it until the buyer is proven: by the claim on a free offering, by the payment on a paid one.
IssuerClientId Optional Your seller client id. The platform resolves the issuer from the API key, so you may omit this; supply it only as a cross-check, because a mismatch is refused with 403 IssuerMismatch.
SuccessUrl Optional Absolute HTTP(S) URL to send the buyer to after payment. A session_id query parameter is appended for you.
CancelUrl Optional Absolute HTTP(S) URL for a buyer who backs out.
Quantity Optional How many seats the purchase is for, a whole number from 1 to 100. Omit it for one seat. The purchase issues one license with that many seats, under one subscription. A Monaiq platform plan is always one and is refused in multiples, and so is an offering whose AllowsMultiple is false (every free plan and trial, and a paid offering the seller set to one seat): 400 CheckoutFailed.
ReturnUrl Optional Absolute HTTP(S) URL to send the buyer to when they are finished, up to 2048 characters and carrying no user information. It is recorded on the checkout and is yours, unlike SuccessUrl, which is where the payment provider returns a payer.
RequestId Optional A GUID identifying this checkout attempt, generated for you when the field is absent. Send the same value on a retry and the existing checkout comes back instead of a second one. Sending the empty GUID explicitly is refused with 400 ValidationError.
BuyerIdToken Optional An OpenID Connect ID token that proves you already verified CustomerEmail. Free and trial offerings only. See Buyer already signed in.

A malformed CustomerEmail is 400 ValidationError. There is no 404 for an address without an account: that refusal is gone.

A Quantity outside 1 to 100, and a ReturnUrl that is relative, uses a scheme other than HTTP or HTTPS, carries user information such as https://good.example.com@evil.example.com, or runs past 2048 characters, are each 400 ValidationError. There is no list of allowed return hosts: you choose where your own buyer lands.

A retry is the same purchase only when it asks for the same number of seats. The same RequestId with a different Quantity is refused with 409 RequestIdInUse, the same answer a request id already used by another buyer gets.

Monaiq bills for the seats and issues one license with a seat for each, every seat with its own credentials and its own allowance, sharing the license’s features. The completed result names the license and every seat, and carries a credential only for a purchase of a single seat; for more than one, issue a credential per seat as you assign it. You decide which user holds which seat, in your own directory.

An offering carries these fields, in the buyer views and in the seller entity: AllowsMultiple (true when a buyer may take more than one seat; always false for a free plan or a trial), AddOn (true when the offering is held alongside a plan rather than as one), both always true or false and never null, and TrialDays (the trial length in days, 1 to 90, present on Trial offerings only). A buyer holds one plan of a product at a time and one license of an offering; the checkout refusals that enforce it are listed with in-app purchase.

A successful create returns 200 with the session. A paid offering carries the Stripe URL:

{ "SessionId": "cs_a1B2...", "SessionUrl": "https://checkout.stripe.com/c/pay/cs_a1B2..." }

Redirect the buyer there to complete payment. The typed address is passed to Stripe as customer_email; the account is resolved or created when the payment settles, and the delivery email deep-links the buyer to their license page, where their first sign-in links the account.

A free or trial offering carries a claim URL instead, and nothing has been provisioned yet:

{ "SessionId": "free-9c1e...", "SessionUrl": null, "ClaimUrl": "https://monaiq.com/claim/..." }
Field Meaning
SessionId What you poll, on both paths.
SessionUrl Stripe Checkout URL; null on the free path.
ClaimUrl Where the buyer accepts the offer. Present only on the free typed-address path, absent everywhere else.

Send the buyer to ClaimUrl now, in the same browser they pressed your button in. The page names you and the product, asks them to confirm, proves the address with a one-time code, and returns them to your ReturnUrl with the session id. The license is created on that confirmation, not before. Do not make them wait for an email: the same link is emailed to the address only as a fallback, some minutes later, for a buyer who closed the tab, and it is good for seven days.

Once they land on ReturnUrl?session_id=..., read the result once: it is already completed. Poll only when you set no ReturnUrl.

If your app signs the buyer in with OpenID Connect before the sale, send their ID token as BuyerIdToken. The claim page then skips the one-time code: it says you verified the address and asks for one confirmation. That confirmation creates the buyer’s Monaiq account, or adds the license to the one they have, and returns them to your ReturnUrl.

First register your issuer under Account settings → Customer sign-in in the portal: the iss of your ID tokens, exactly as it appears, and your app’s client ID. The token is accepted when:

  • iss is the registered issuer, and its signature verifies against the keys that issuer’s /.well-known/openid-configuration publishes (RS, PS or ES algorithms).
  • aud is the registered client ID.
  • exp has not passed, allowing two minutes of clock skew, and iat is present.
  • email is CustomerEmail (case does not matter). Request the email scope. If the token carries email_verified, it is true.

The page honours the proof for one hour from the token’s iat. After that, or with a token that fails any check, the checkout behaves exactly as it does without the field: the response is the same, and the buyer proves the address with the emailed code. A bad token is never an error. The claim email is still sent as the fallback. A proof creates no portal session: a buyer with no ReturnUrl lands on their license page and signs in there as usual.

A signed assertion under your API key is not accepted as a proof. Your API key already authenticates the request, so a signature with the same key would add nothing a third party could check.

A repeated create with the same RequestId returns the existing session; on the free path that includes a freshly issued ClaimUrl, and no second email is sent. A RequestId that belongs to a different buyer or seller is 409 RequestIdInUse.

A create with a new RequestId for the same seller, offering and address while the claim is still open returns the same session with a new ClaimUrl, and sends the email again with that link. Every link issued for an open claim keeps working until the buyer accepts through one of them; the others then read “already accepted”.

Claim notices are capped at five per address per hour across all sellers, re-sends included, and one open intent is kept per seller, offering and address. Over the cap the checkout still succeeds and the link is still returned; only the email is withheld.

POST /licensing/checkout/session/result with the session id:

{ "SessionId": "cs_a1B2..." }

A known session always answers 200, and Status tells you where it stands:

{
"Status": "completed",
"CorrelationId": "order-8842",
"EncodedCredential": "SIDUB_LIC_...",
"LicenseId": "7b0e5d10-0000-0000-0000-00000000a001",
"SeatIds": [ "c9d2a7e1-0000-0000-0000-000000000000" ],
"OfferingId": "b3b1f6e0-0000-0000-0000-000000000000"
}
Status Meaning
pending Nothing terminal has happened yet: payment has not settled, or the buyer has not accepted the claim. Poll again; the response carries a Retry-After: 3 header (seconds).
completed The license exists. LicenseId is the license and SeatIds its live seats, oldest first. For a purchase of one seat, EncodedCredential is the buyer’s credential; treat it as the secret it is. For more than one seat it is null: issue a credential per seat.
failed The checkout did not complete; an Error field carries the reason. A claim whose seven days lapsed reads "Claim expired."

The status vocabulary is exactly these three values and does not change with the claim path.

An unknown or expired session id answers 404 NotFound.

The management API is at /licensing-mgmt and takes the account API key in dub-apiKey. Your account is the issuer of every license it sells, and the key is what says so: the platform resolves the issuer from the key alone. A license or seat another account issued answers 404 NotFound, the same answer an id that does not exist gets, so this surface never tells you what exists outside your own account.

A license is created by a purchase (a checkout on the marketplace, in the portal or in your own app), never by this API, and invoices are portal work: there is no invoice operation here. A refund is made as part of a cancel (below) or from the invoice in the portal. The portal’s Customers page reads the same licenses and seats, and makes the same suspend, resume, cancel and allowance changes, without code.

A license is what a customer bought: its term, price, features, and how it is billed. A seat is one unit of a license: its own keys, its own license codes, its own allowance. A license of 25 seats has 25 seats naming it, and the seat count is the number of live seats.

Every list on this API answers one page at a time, in the same shape:

{ "Items": [...], "Count": 100, "ContinuationToken": "MS5..." }
Query Meaning
?top=<n> How many rows the page holds at most: a whole number from 1 to 500. Omitted, 100.
?continuationToken=<token> Resumes the list after the page that issued the token. Omitted, the first page.

Items is the page’s rows in the list’s order, Count is how many rows this page holds, and ContinuationToken is what resumes the list, or null when nothing follows the page. Read every page by repeating the call with the token until it is null; each row arrives exactly once. The license list may end with an empty page when its row count is a multiple of top; the seat and credential lists never do. Send the token URL-encoded and treat it as opaque: it is bound to the list it came from and to every filter in force, and one replayed on another list, with other filters or by another account is 400 InvalidPaging, as is a top out of bounds. top may change between pages.

A caller that sends neither parameter still gets the first page: up to 100 rows. A list of fewer than 100 rows is therefore one page, as before, with ContinuationToken: null.

GET /licensing-mgmt/licenses lists the licenses you issued, paged, in a stable order by id. Optional filters narrow it, in storage, and combine:

Query Finds
?correlationId=<value> The licenses carrying your own id.
?clientId=<guid> Every license one customer holds from you.
?status=<Active | Suspended | Cancelled> The licenses in that state.
?productId=<guid> The licenses made against one of your products.
?offeringId=<guid> The licenses made against one offering.

correlationId is matched exactly, after trimming, and is data: a quote or a percent sign in it is matched as the character it is. Send the value URL-encoded. clientId and correlationId at once, an issuerClientId parameter, a correlationId past 128 characters, a status that is not one of the three, or a productId or offeringId that is not a GUID is 400 InvalidFilter.

GET /licensing-mgmt/licenses/{licenseId} reads one license. Each license carries:

Field Meaning
Id The license id: a GUID.
ClientId The customer.
DisplayName What was bought.
CorrelationId Your own id for it, or null.
LicenseClassification Trial, Subscription or Perpetual.
Status Active, Suspended or Cancelled.
Standing Where the license stands now: Active, Ending (a trial, a scheduled cancellation or a term that ends within two weeks), Suspended, PaymentFailed or Ended.
SuspendedBy Who suspended it: Seller, Monaiq or Payment. A buyer never suspends a license. null while it is not suspended.
SuspensionReason The reason code, a LicenseEventReason name. A seller’s suspension carries Misuse, PaymentDispute, BuyerRequest or Other, and a failed payment PaymentFailed. null while it is not suspended.
SuspensionReasonLabel The seller-facing words for the code. null while it is not suspended.
TermEndUtc When the license’s term ends, for every seat alike, or null when it does not end on its own.
PaidThroughUtc How far it has been paid. A paid renewal moves it to the end of the service period that payment bought. Access runs to the later of the term and this, plus a 72-hour renewal grace.
ScheduledCancellationUtc When it is scheduled to end, or null. Access stops then.
ProductId, OfferingId, BaseRate, Currency, Interval, IntervalUnit What was bought and how it is billed.
Features What every seat of the license is entitled to (license features).

A 404 NotFound at any of these routes reads "License '{id}' not found.".

PATCH /licensing-mgmt/licenses/{licenseId} changes the license’s display name, its features, or both: the one body a seat’s rename cannot also do. Features is the license’s complete new set: a feature the license holds that the body omits is removed, and every seat reads the new set at its next authorization. Nothing else about a license moves through this route: its seats move on their own routes below, and its status on the lifecycle routes.

{ "DisplayName": "Acme Pro (renewed)", "Features": [] }

Sending neither field is 400 ValidationError, “Nothing to change: send DisplayName, Features, or both.”, and a blank DisplayName is refused the same way.

To change one feature, read the license, edit that entry, and send the whole list back. Each entry is matched to a feature the license holds by its id, or, when it has none or names one this license does not hold, by its type and FeatureKey; an entry that matches nothing is added, and a held feature no entry matches is removed. A feature whose values are unchanged keeps its id; a changed feature is answered with a new id. Two entries with the same id, or two of one type with the same FeatureKey, are 400 ValidationError.

A feature is one entitlement, of one of four types, named by __sidub_entityType. A seat’s signed authorization carries features in this same shape. The MCP catalog tools use the same field for a product feature’s type, with product type names such as ProductFeature.ProductQuotaFeature. A license’s features look like this:

"Features": [
{ "__sidub_entityType": "ServiceAccessLicenseFeature", "FeatureKey": "reports", "ServiceAccessLevel": "Allowed" },
{ "__sidub_entityType": "RateLimitLicenseFeature", "FeatureKey": "api-calls", "SampleSeconds": 60, "RateLimit": 100 },
{ "__sidub_entityType": "QuotaLicenseFeature", "FeatureKey": "exports", "Allowance": 1000 },
{ "__sidub_entityType": "LimitLicenseFeature", "FeatureKey": "projects", "Limit": 10 }
]
Type Fields you send Grants
ServiceAccessLicenseFeature FeatureKey, ServiceAccessLevel (Allowed or Denied) Access to a capability, or not.
RateLimitLicenseFeature FeatureKey, SampleSeconds, RateLimit At most RateLimit uses in any SampleSeconds window, in one process.
QuotaLicenseFeature FeatureKey, Allowance Allowance uses per billing period, for each seat. An Allowance of 0 has no cap: every use is counted and reported, and none is refused.
LimitLicenseFeature FeatureKey, Limit (a whole number of 0 or more, or null for unlimited) A number the application holds to, such as 10 projects or 90 days of history. The application counts its own data and asserts the count; the platform never counts it.

A LimitLicenseFeature is read by SDK 2.4.0 or later. An older .NET SDK (before 2.4.0) fails the whole authorization for a license carrying a Limit, not just that feature, so give a Limit only to offerings whose application runs SDK 2.4.0 or later.

A response names the type in its qualified form (default:LicenseFeature.QuotaLicenseFeature), which a request may also send. It adds fields the platform owns: id (the current version of that feature: it changes when the feature’s values change, and a feature is identified by its type and FeatureKey), and LicenseId and IssuerClientId (set from the route and your key; values sent are ignored). An allowance carries the entitlement alone: what a seat has used is on the platform’s ledger, and a runtime spends it through allowance grants.

POST /licensing-mgmt/licenses/{licenseId}/seats adds seats to a license. It is a call of its own because it charges your customer, and an operation that moves money should never be indistinguishable from renaming something.

POST /licensing-mgmt/licenses/7b0e5d10-0000-0000-0000-00000000a001/seats HTTP/1.1
Host: api.monaiq.com
dub-apiKey: <your API key>
Content-Type: application/json
{ "Count": 15, "RequestId": "5f2d0c44-0000-0000-0000-00000000a0d1" }

Count is how many seats to add (a change, not a total) and must be a whole number of at least one. The subscription moves first and the seats are minted after it, so what is issued is always what is billed. Adding seats charges your customer’s card now, with the payment processor’s default proration for the remainder of the current period. There is no confirmation step, so ask for one in your own application before you send this.

RequestId is optional and is how you repeat the call safely. Generate a GUID per request and send the same value on every retry: the first call records how many seats the license is to hold once it is done, and a later call with the same id completes that number instead of adding again, so a retry after a lost answer never bills or mints the seats twice. A repeat answers the seats the license then holds, with Seats listing only what that call minted (usually nothing). Without a RequestId a retried add adds again. The empty GUID, or a value that is not a GUID, is refused with 400 ValidationError.

The response is 200:

{
"Previous": 25,
"Current": 40,
"Seats": [ { "Id": "…", "LicenseId": "7b0e5d10…", "RetiredUtc": null, "…": "…" } ]
}
Field Meaning
Previous Live seats before the call.
Current Live seats after it.
Seats The new seats, in the seat shape below. Each has no credential yet; issue one per seat as you assign it.

A refusal changes nothing at all, at the processor or here:

Refusal Meaning
400 ValidationError Count is missing, not a whole number, below one, or would take the license past 100 seats.
404 NotFound No license you issued has that id.
409 LicenseNotActive The license is paused, suspended or cancelled. Resume it first.
409 SeatsNotSupported A Monaiq platform plan, a plan sold one seat at a time (the offering’s AllowsMultiple is false, as on every free plan and trial), or a one-time purchase: the buyer adds seats to a one-time license from their Monaiq license page.
409 RequestIdInUse The RequestId was already used on this license with a different Count. This call changed nothing; the earlier request stands, so list the seats before sending another.
409 RequestInProgress The same RequestId is still being completed by an earlier call that has not answered yet. Repeat the call in a moment and it answers what that call did.
502 SubscriptionUpdateFailed The processor did not make the change. Nothing was written.

GET /licensing-mgmt/licenses/{licenseId}/seats lists the seats of one license, live and retired, oldest first, paged:

{
"Items": [
{
"Id": "c9d2a7e1-0000-0000-0000-000000000000",
"LicenseId": "7b0e5d10-0000-0000-0000-00000000a001",
"ClientId": "a1b2c3d4-0000-0000-0000-000000000000",
"Name": "Acme Pro",
"RetiredUtc": null,
"LicenseStatus": "Active",
"IssueDate": "2026-01-04T10:15:00Z",
"ServiceKeyId": "3f8e1c22-0000-0000-0000-000000000000",
"ServiceKeyPublic": "MFkwEwYHKoZ...",
"Allowances": [
{
"FeatureKey": "exports",
"Allowance": 100,
"Uncapped": false,
"Used": 12,
"Reserved": 18,
"Remaining": 70,
"PeriodEndUtc": "2026-11-01T00:00:00Z"
}
]
}
],
"Count": 1,
"ContinuationToken": null
}
Field Meaning
LicenseId The license this seat belongs to. Every seat of one license carries the same value.
LicenseStatus The seat’s license’s status, or null when it could not be read.
RetiredUtc When the seat was retired, or null while it is live. A retired seat is refused at its next authorization and keeps no license codes.
Allowances The seat’s figures for each allowance of its license in the current billing period.

A seat carries no features: it is entitled to its license’s, read at GET /licensing-mgmt/licenses/{licenseId}. What it has used of each allowance is its own:

Allowances[] field Meaning
FeatureKey The allowance feature.
Allowance The uses per billing period. Zero or less is no cap.
Uncapped true when the allowance has no cap: every use is in Used, none is refused, and Remaining is 0 because there is nothing to count down.
Used The uses recorded for the seat in the period.
Reserved Units the seat’s runtimes hold and have not yet recorded as used.
Remaining The uses neither recorded nor held: Allowance plus Credited, less Used and Reserved, never below zero. 0 when the allowance has no cap.
Credited Units you credited to the seat for the period (see Adjusting a seat’s allowance). 0 when there are none.
PeriodEndUtc When the period resets.

The figures are read from the platform’s count on each call, for every live seat, whether or not any application has run under the seat this period. Allowances is empty for a retired seat, for a license with no allowance, and for an allowance whose figures could not be read. It is filled on the two reads only: the responses of add, rename and retire carry it empty.

GET /licensing-mgmt/licenses/{licenseId}/seats/{seatId} reads one seat in the same shape. A seat named under a license it does not belong to answers 404 NotFound, the same answer an unknown seat gets.

PATCH /licensing-mgmt/licenses/{licenseId}/seats/{seatId} renames a seat. The body carries only { "Name": "..." }; any other field is ignored, so sending a whole seat object back never moves its license, its retirement or its keys, however stale it has become. A missing or blank Name is 400 ValidationError, “Name is required: the seat’s new name.”

POST /licensing-mgmt/licenses/{licenseId}/seats/{seatId}/retire retires one seat of a license you issued. With no body it retires the seat and nothing more.

POST /licensing-mgmt/licenses/7b0e5d10-0000-0000-0000-00000000a001/seats/c9d2a7e1-0000-0000-0000-000000000000/retire HTTP/1.1
Host: api.monaiq.com
dub-apiKey: <your API key>

On a license billed through a Stripe subscription, the subscription moves first and the seat follows it: your customer is credited with the processor’s default proration for the rest of the current period. Every retire revokes the seat’s license codes and refuses the seat at its next authorization. The response is 200 with the retired seat, its RetiredUtc set. A retired seat cannot be brought back; add a seat instead. Retiring a seat that is already retired changes nothing and answers 200 again, so a retry cannot credit or refund your customer twice.

A body makes it your cancel of the seat, which the customer is told about:

{ "Refund": "Full", "Reason": "BuyerRequest", "Note": "As you asked on the phone." }

Refund is None (the default) or Full, by name in any case. Full is for a license bought outright: the seat’s price, as the payment that bought it paid it, goes back to the card that paid, never more than the license’s seat rate, and the response carries a Refund block (Amount, Currency, InvoiceId, RefundId, Failure). A seat on a subscription is credited by the processor instead, and one on a license Monaiq invoices is billed one seat fewer from then on, so Full is refused on both. Reason takes the cancel’s codes (below), Other with a Note, and Note up to 500 characters. With a body, the customer is emailed which seat stopped, your reason and note, and the refund. A refund the processor refuses after the seat is retired leaves the seat retired; Refund.Failure says why, and you refund it from the invoice in the portal.

The license’s last live seat cannot be retired. To end the license, cancel it.

Refusal Meaning
404 NotFound No seat you issued has that id under that license.
400 InvalidRefund Refund is not None or Full.
400 InvalidReason Reason is not one of the cancel’s codes.
400 NoteRequired Reason is Other and there is no Note.
400 InvalidPayload The body cannot be read as JSON.
413 PayloadTooLarge The body is past 4 KB.
409 LicenseNotActive The license is paused, suspended or cancelled. Resume it first.
409 LastSeat This is the license’s last live seat. Cancel the license instead.
409 NotRefundable Full was asked and the license cannot give it. Nothing was retired.
502 SubscriptionUpdateFailed The processor did not make the change. Nothing was written.

POST /licensing-mgmt/licenses/{licenseId}/seats/{seatId}/allowance-adjustments corrects one seat’s position in an allowance for the current billing period: give back units for work that failed on your side, grant a goodwill top-up, or reset the period’s count. Each adjustment is recorded with its reason. Nothing is billed, and Used stays the uses recorded; the seat’s Credited rises instead, and Remaining with it.

POST /licensing-mgmt/licenses/7b0e5d10-0000-0000-0000-00000000a001/seats/c9d2a7e1-0000-0000-0000-000000000000/allowance-adjustments HTTP/1.1
Host: api.monaiq.com
dub-apiKey: <your API key>
Content-Type: application/json
{ "FeatureKey": "documents", "Credit": 20, "Reason": "Our outage failed 20 extractions", "RequestId": "5d6f1c2a-0000-0000-0000-000000000001" }
Field Required Meaning
FeatureKey Yes The allowance feature, a capped allowance of the license.
Credit One of the two Whole units to credit the seat for the period, 1 to 1,000,000. A credit can take the seat past its allowance for this period.
Reset One of the two true credits what the seat has used this period and not already had credited, so its count starts again from zero.
Reason Yes Why, up to 500 characters. Stored with the adjustment.
RequestId Yes A GUID that names this adjustment. Repeat it on a retry and the adjustment is made once.

The response is 200 with Adjustment (Id, FeatureKey, Kind (Credit or Reset), Units, Reason, RequestId, PeriodStartUtc, PeriodEndUtc, CreatedUtc) and Allowance, the seat’s figures for the allowance afterwards, in the Allowances[] shape above. A credit applies to the current period only; the next period starts from the allowance again. Runtimes see the credited units on their next grant.

GET on the same path lists the current period’s adjustments of the seat’s allowances, oldest first, under Items.

Refusal Meaning
400 ValidationError A field is missing or out of range, both or neither of Credit and Reset are stated, or FeatureKey is not a capped allowance of the license.
404 NotFound No seat you issued has that id under that license.
409 SeatRetired The seat is retired.
409 RequestIdInUse The RequestId was already used for a different adjustment of this seat. Nothing was changed.

POST /licensing-mgmt/licenses/{licenseId}/cancel ends a license, and gives your customer’s money back as part of it. Refund says how much, and that sets when access stops:

Refund On a Stripe subscription On a license bought outright On a free plan, a trial, or a license Monaiq invoices
None (the default) Ends at the end of the period already paid for. Ends now. A free plan or trial ends now; an invoiced license ends at its period end.
Prorated Ends now; the unused share of the current period’s charge goes back. Not offered. Not offered.
Full Ends now; the current period’s charge goes back in full. Ends now; every payment for the license and its added seats goes back, never a plan change’s. Not offered.

On a subscription the refund is of what the period cost: usage billed on the same invoice was consumed and is not refunded, and a license’s usage so far is still billed. A license that ends at its period end keeps working until then and reads Active with a ScheduledCancellationUtc; your customer cannot undo a cancel you scheduled. A cancel with no refund of a subscription your customer already set to end keeps their date and makes the cancel yours: they are emailed your reason and can no longer undo it. Any cancelled license stops working, a license bought outright included. The subscription moves first; a processor refusal is 502 SubscriptionUpdateFailed and nothing is written. A refund the license no longer offers (it was refunded meanwhile, or the period ran out) is 409 RefundNotOffered, and nothing is written. The refund is asked for after the license ends; the response carries a Refund block (Amount, Currency, InvoiceId, RefundId, Failure). A refund the processor refuses leaves the license cancelled with Failure saying why, for you to refund from the invoice in the portal; one whose answer never came says so in Failure, because it may have gone through: check the invoice before refunding again. A license already cancelled answers 200 as it stands and refunds nothing. An API key can refund your customer’s own payments only as part of a cancel, and only back to the card that paid.

POST …/suspend stops access without ending the license, and POST …/resume restores it. The response to all three is 200 with the license. A cancelled license is neither suspended nor resumed: 409 InvalidState. The body of cancel and suspend is optional, but one that is present and cannot be read as JSON is 400 InvalidPayload, and one past 4 KB is 413 PayloadTooLarge; neither is silently defaulted.

Suspend and cancel each take an optional Reason and Note, which the customer sees:

{ "Refund": "Prorated", "Reason": "Refunded", "Note": "Refunded the rest of October." }
Route Reason codes
…/suspend PaymentDispute, Misuse, BuyerRequest, Other
…/cancel BuyerRequest, Refunded, PaymentDispute, Misuse, Other

A code is matched by name, in any case, and so is a Refund word; one not in the route’s list is 400 InvalidReason or 400 InvalidRefund. Other says nothing on its own, so it needs a Note; without one the call is 400 NoteRequired. A call without a reason records none, and its email carries no reason line. Note is up to 500 characters; a longer one is cut. The customer is emailed once for suspend, resume and cancel: what changed, when access ends, your reason and note, what goes back to their card, and your support contact.

Three operations hang off a seat of a license you issued. All three answer 404 NotFound for a seat another account issued, or one named under the wrong license.

POST /licensing-mgmt/licenses/{licenseId}/seats/{seatId}/credentials issues a runtime credential (a license code) and returns 201 with the encoded string. An optional body carries a Label of up to 60 characters, which is a note to you and appears in the listing, and an optional RequestId (a GUID of your own) that makes the call safe to repeat: the same seat and RequestId answer the one credential already issued, re-encoded, rather than a second live credential. A longer label is trimmed rather than refused, a body that is not JSON is 400 InvalidPayload, a RequestId that is not a GUID is 400 ValidationError, and a request body larger than 4 KB is 413 PayloadTooLarge; in each case nothing is issued. A retired seat is issued no license code: 409 SeatRetired.

{
"CredentialId": "8b7a6c55-0000-0000-0000-000000000000",
"SeatId": "c9d2a7e1-0000-0000-0000-000000000000",
"ClientId": "a1b2c3d4-0000-0000-0000-000000000000",
"IssuedUtc": "2026-01-04T10:15:00Z",
"Source": "api",
"Label": "build server",
"EncodedCredential": "SIDUB_LIC_..."
}

EncodedCredential is shown once. The token inside it is not stored and cannot be derived again, so keep it when you receive it. The response carries Cache-Control: no-store for the same reason. If you lose one, issue another and revoke the old one.

GET /licensing-mgmt/licenses/{licenseId}/seats/{seatId}/credentials lists what the seat has, newest first, paged. Each item carries CredentialId, SeatId, ClientId, IssuedUtc, RevokedUtc, Source, Label and IsActive. A listing never carries an encoded credential. Revoked credentials stay on the list, because removing them would leave you unable to tell a revoked credential from one that never existed.

DELETE /licensing-mgmt/licenses/{licenseId}/seats/{seatId}/credentials/{credentialId} revokes one and returns 204. It is idempotent: revoking one that is already revoked answers 204 again. A credential that belongs to a different seat answers 404 NotFound, even when both seats are yours.

What revoking does, and when, is in Credentials and keys.

POST /licensing/runtime/GenerateLicenseAuthorization is the call behind the SDKs’ getAuthorization / GetAuthorization. It takes the runtime token as a bearer, and the body names the seat:

{
"RequestId": "5f2d0c44-0000-0000-0000-000000000000",
"SeatId": "c9d2a7e1-0000-0000-0000-000000000000",
"IssuerClientId": "a1b2c3d4-0000-0000-0000-000000000000",
"ProductId": "d4c3b2a1-0000-0000-0000-000000000000",
"SignatureLayout": 1
}
Field Required or optional Meaning
RequestId Required A GUID, new for every request and repeated only when the same request is retried.
SeatId Required The seat the token names.
IssuerClientId Optional The seller the application declares itself to belong to. A seat of another seller is refused 403 AuthorizationDenied with the reason ProductMismatch. The SDKs send IssuerClientId from their options.
ProductId Optional The product the application declares itself to be. A seat of another product is refused the same way.
SignatureLayout Optional The newest signature layout the caller reads (1 today). The answer is signed in that layout or an older one and names it in its own SignatureLayout, beside the signature. A caller that states none is answered as before 2.2.0, with no layout field.

The three optional fields are the same on AcquireAllowanceGrant and ReadAllowance below. A request that declares neither seller nor product is answered as before.

A successful call returns 200 with the signed authorization: authorization id, seat id, license id, classification (numeric on the wire: 0 trial, 1 subscription, 2 perpetual), issue and expiry dates, the feature list and, when the request stated a SignatureLayout, the layout the answer was signed in. The SDKs verify its signature before trusting it. Each allowance feature carries its Allowance only: what the seat has used is on the platform’s ledger, not on the authorization. An enforcement denial answers 403 AuthorizationDenied with the reason as Details[0].Code: Expired, LicenseSuspended, LicenseCancelled, SeatRetired or ProductMismatch (the credential was not issued for the seller or product the application declares). The .NET SDK raises it as LicenseAuthorizationDeniedException with that Reason; the JavaScript SDK exposes it as LicensingError.denialReason.

The body’s SeatId must be the seat the token names, or the answer is 404 NotFound (never a 403, which would confirm the id exists elsewhere). A credential authorizes its own seat and no other.

A runtime spends an allowance from a grant: a signed, one-hour reservation of units taken from the seat’s ledger for the billing period. The SDKs take and return grants for you; Enforcing an allowance covers the model. Every operation here takes the runtime token as a bearer, and the body’s SeatId must be the seat the token names, as on GenerateLicenseAuthorization.

POST /licensing/runtime/AcquireAllowanceGrant reserves units of one allowance feature:

{
"RequestId": "8d1f6a2e-0000-0000-0000-000000000000",
"SeatId": "c9d2a7e1-0000-0000-0000-000000000000",
"FeatureKey": "exports",
"Amount": 1,
"PriorGrantId": "41c7e9b0-0000-0000-0000-000000000000",
"PriorSpent": 100
}
Field Required or optional Meaning
RequestId Required A GUID, new for every request and repeated only when the same request is retried. The grant’s id is derived from it, so a retried request answers the grant it already took.
SeatId Required The seat to take units from.
FeatureKey Required The allowance feature.
Amount Optional The size of the use the grant must cover. Omit it for one.
PriorGrantId Optional A grant the runtime has finished spending. Its unspent units return to the ledger in the same call.
PriorSpent Optional The uses the runtime spent of PriorGrantId.
IssuerClientId, ProductId, SignatureLayout Optional As on GenerateLicenseAuthorization.

A grant is a tenth of what the period has not yet granted, at least one unit and at least Amount, and never more than the period has left. The first grant of a period is a tenth of the allowance; each later grant, while earlier ones are held, is a tenth of the rest, so runtimes sharing a seat take many grants before an unspent period is refused. A successful call returns 200 with the signed grant:

Field Meaning
id The grant id.
IssuerClientId, SeatId, LicenseId, FeatureKey The seller, the seat the units were taken from, its license, and the allowance feature.
PeriodStartUtc, PeriodEndUtc The billing period the units belong to. The start is inclusive and the end exclusive.
Allowance The feature’s allowance when the grant was issued.
Units The uses the grant covers.
IssuedUtc, ExpiresUtc When the grant was issued, and when it stops covering uses: one hour after issue, or the period end if sooner.
PeriodGranted The units handed out in the period when the grant was issued, this grant included.
PeriodUsed The uses recorded in the period when the grant was issued.
Spent The uses the platform has attributed to the grant from consumption reports.
ReportedSpent The uses the runtime said it spent when it returned the grant.
Released Whether the unspent units have returned to the ledger.
SignatureLayout The layout the grant was signed in, present when the request stated one. Not covered by the signature.
__sidub_entitySignature The signature, made with the seat’s service key. The SDKs verify it before spending from the grant.

A feature whose Allowance is zero or less has no cap: its grant carries Units of 0, covers any number of uses and reserves nothing.

Refusal Meaning
403 AllowanceExhausted The period has no units left. Details[0] is { "Code": "AllowanceExhausted", "TargetId": "<feature key>" }, and Details[1] is { "Code": "PeriodEndUtc", "Message": "<ISO-8601 UTC instant>" }: when the period resets.
403 AuthorizationDenied The seat’s authorization is refused, with the reason as Details[0].Code, as on GenerateLicenseAuthorization. A suspended, cancelled or expired license, or a retired seat, is granted nothing from that moment.
404 FeatureUnknown The license has no allowance feature with that key.
404 NotFound The seat is not the one the token names.
400 ValidationError RequestId is missing or the empty GUID.
400 InvalidPayload The body could not be read.
429 LedgerBusy The seat’s ledger was written by other runtimes on every attempt. Retry the same request after Retry-After (1 second).
503 StoreUnavailable The store is still connecting. Retry after Retry-After (5 seconds).

POST /licensing/runtime/ReleaseAllowanceGrant returns a grant’s unspent units to the ledger:

{
"RequestId": "2b7c4f10-0000-0000-0000-000000000000",
"SeatId": "c9d2a7e1-0000-0000-0000-000000000000",
"GrantId": "41c7e9b0-0000-0000-0000-000000000000",
"Spent": 37
}

Spent is the uses the runtime spent of the grant. The response is 200 with the grant, its Released set to true. The call is idempotent: returning a grant twice changes nothing and answers 200 again. A grant the seat does not hold answers 404 NotFound, and a missing or empty RequestId answers 400 ValidationError. A lost ledger race is 429 LedgerBusy with Retry-After, as on acquire. A credential revoked since the grant was taken may still return it: the seat gets its units back at once rather than when the grant expires.

The units of a grant that is not returned stay reserved until the platform reclaims the grant: on the seat’s next acquire, once the grant has been expired for five minutes.

POST /licensing/runtime/ReadAllowance reads a seat’s figures for one allowance feature in the current billing period. It reserves nothing and changes nothing:

{
"RequestId": "5e0a9c31-0000-0000-0000-000000000000",
"SeatId": "c9d2a7e1-0000-0000-0000-000000000000",
"FeatureKey": "exports"
}

A successful call returns 200 with the signed figures:

Field Meaning
SeatId, FeatureKey The seat and the allowance feature read.
PeriodStartUtc, PeriodEndUtc The billing period. The start is inclusive and the end exclusive.
Allowance The feature’s allowance. Zero or less is no cap.
Used The uses recorded in the period.
Granted The units handed to the seat’s runtimes in the period and not returned; never below Used. What is above Used is held by runtimes and not yet recorded as used.
AsOfUtc When the figures were read.
SignatureLayout The layout the reading was signed in, present when the request stated one.
__sidub_entitySignature The signature, made with the seat’s service key. The SDKs verify it before showing the figures.

The SDKs call it when the state is read and the runtime holds no figures for the period, or figures more than a minute old: Showing what is left. It answers 403 AuthorizationDenied, 404 FeatureUnknown, 404 NotFound, 400 ValidationError and 400 InvalidPayload as AcquireAllowanceGrant does. It never answers AllowanceExhausted: a spent period reads Granted equal to Allowance.

POST /consumption/runtime/messages is the ingestion endpoint for metered usage. It takes the runtime token as a bearer. Both SDKs batch and send consumption for you once ConsumptionServiceUri / consumptionServiceUri is configured. It is not designed for hand-rolled calls; Consumption and metering covers the model.

A use spent from an allowance grant carries the grant’s id as a top-level GrantId, and the platform attributes the use to that grant.

The gateway binds the batch to the token: a batch whose SeatId is not the seat the credential names is refused 400 before it reaches ingestion, and the credential id is stamped onto the batch so usage reported under a revoked credential is dropped rather than billed.

Monaiq issues the runtime tokens, so it publishes its own discovery documents. Both are anonymous and both are public by construction: an issuer string, a URL, and the elliptic-curve public points:

  • GET /licensing/runtime/.well-known/openid-configuration
  • GET /licensing/runtime/.well-known/jwks

Signing key versions are additive and never disabled, so the JWKS serves every version that has ever signed. You do not need either document to integrate; the SDKs never fetch them, but they are there for a proxy or a gateway of your own that wants to verify a token itself. The per-environment URLs are in Endpoints.

Per seat, keyed on the seat the token names:

Path Limit
POST /licensing/runtime/GenerateLicenseAuthorization 600 requests per minute, one bucket shared by the four runtime operations
POST /licensing/runtime/AcquireAllowanceGrant (the same bucket)
POST /licensing/runtime/ReleaseAllowanceGrant (the same bucket)
POST /licensing/runtime/ReadAllowance (the same bucket)
POST /consumption/runtime/messages 2400 requests per minute

A coarser per-IP limit runs ahead of token validation as a volume guard: 6000 requests per minute per caller address across the four /licensing/runtime/ operations together, and 12000 per minute across /consumption/runtime/messages. Each API keeps its own counters, so metering traffic never draws on the authorization budget, per seat or per address. A refused request answers 429 with Retry-After; the SDKs honour it.

The seller API (/licensing/checkout/* and /licensing-mgmt) is limited to 300 requests a minute per account API key. The anonymous purchase reads under /purchase are limited to 60 requests a minute per caller address, as are the discovery documents under /licensing/runtime/.well-known/. The MCP endpoint is limited to 1200 requests a minute per caller address ahead of sign-in and 300 a minute per signed-in person. Every limit is set per environment, not per seller.