HTTP API
This page 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.
Authentication
Section titled “Authentication”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.1Host: api.monaiq.comContent-Type: application/jsondub-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.1Host: api.monaiq.comContent-Type: application/jsonAuthorization: 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.
Correlation ids
Section titled “Correlation ids”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.
Failures: the ApiError shape
Section titled “Failures: the ApiError shape”Every endpoint returns one JSON shape on failure. This is the product-wide contract — it applies across the platform’s HTTP surface, including the invoice and billing endpoints:
{ "Code": "ValidationError", "Message": "RequestId, OfferingId, CorrelationId, and CustomerEmail are required.", "CorrelationId": "0f8fad5bd9cb469fa16570867728950e", "Details": [ { "Code": "ValidationError", "Message": "CustomerEmail is required.", "Field": "CustomerEmail" } ]}Codeis machine-readable and PascalCase. Branch on it, never onMessage.Messageis for humans; its wording may change without notice.CorrelationIdrepeats theX-Correlation-Idheader value.Detailsis optional: per-field entries, present on validation failures.
| Code | HTTP status | Meaning |
|---|---|---|
ApiKeyMissing |
401 | No dub-apiKey header on the request. |
ApiKeyInvalid |
403 | The key does not resolve to an account. |
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 license exists but authorization is refused — expired, or its billing plan is suspended or cancelled. Message carries the reason. |
NotFound |
404 | Nothing here for that identifier. |
InvalidPayload |
400 | The request body could not be read at all. |
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 already belongs to a checkout for another buyer or seller. |
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.
Embedded checkout
Section titled “Embedded checkout”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"}| 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. |
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. |
A malformed CustomerEmail is 400 ValidationError. There is no 404 for an address without an
account: that refusal is gone.
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. |
The same claim link is emailed to the address, subject “Accept your {product} license from {seller}”, and it is good for seven days. Show your copy of it beside whatever “check your email” message your buyer is looking at. The license is created when the buyer opens the link, signs in and presses Accept — not before.
A repeated create with the same RequestId returns the existing session; on the free path that
includes a freshly minted ClaimUrl, and no second email is sent. A RequestId that belongs to a
different buyer or seller is 409 RequestIdInUse.
Claim notices are capped at five per address per hour across all sellers, 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.
Polling the result
Section titled “Polling the result”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": "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. EncodedCredential is the buyer’s credential — treat it as the secret it is. |
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.
License authorization
Section titled “License authorization”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 license:
{ "RequestId": "5f2d0c44-0000-0000-0000-000000000000", "LicenseId": "c9d2a7e1-0000-0000-0000-000000000000"}A successful call returns 200 with the signed authorization — authorization id, license id,
classification (numeric on the wire: 0 trial, 1 subscription, 2 perpetual), issue and
expiry dates, and the feature list — whose signature the SDKs verify before trusting it. An
enforcement denial answers 403 AuthorizationDenied with the reason in Message.
The body’s LicenseId must be the license the token names, or the answer is 404 NotFound (never
a 403, which would confirm the id exists elsewhere). A credential authorizes its own license and no
other.
Consumption
Section titled “Consumption”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.
The gateway binds the batch to the token: a batch whose LicenseId is not the license 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.
Runtime discovery
Section titled “Runtime discovery”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-configurationGET /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.
Rate limits on the runtime paths
Section titled “Rate limits on the runtime paths”Per license, keyed on the license the token names:
| Path | Limit |
|---|---|
POST /licensing/runtime/GenerateLicenseAuthorization |
300 requests per minute |
POST /consumption/runtime/messages |
1200 requests per minute |
A coarser per-IP limit runs ahead of token validation as a volume guard.
Related
Section titled “Related”- Endpoints for the base URLs these paths hang off.
- Credentials and keys for where each credential comes from.
- .NET client reference and Node client reference for the SDK surfaces that wrap these calls.