External software-licensing integration¶
Bazaar is FlexGalaxy’s marketplace for edge-deployable software (applets and OS-layer firmware today). Its core loop is Publish → Get → Assign → Install → Run → Pay: a developer publishes an installable with listing, pricing, and rollout rules; a tenant admin subscribes an account and assigns the installable to a specific terminal; the terminal pulls the signed artifact and validates a per-(terminal, installable) license certificate at startup; the developer is paid — without any actor touching the underlying signing, PKI, distribution, or billing systems directly.
Most offers are approved inside Bazaar by the tenant admin. This page documents the alternative: offers whose approval is delegated to an external licensing authority — the use case an outside integrator (an ISV, a compliance desk, or a partner licensing system) implements.
When this applies¶
Each offer carries an approval policy. When an offer’s approval policy is
EXTERNAL, every license request against that offer is routed to a nominated
external authority for the approve / refuse decision instead of being
decided by the tenant admin. Bazaar remains the system of record and performs
issuance after an approval; the external authority owns only the decision.
The authority is a distinct account from the publisher: whoever publishes the
software and whoever decides its licensing are separate roles, and an EXTERNAL offer
names its authority account explicitly. You may also hand the decision back to the
customer for some or all accounts while remaining the authority of record — see
Delegating approval to the owner.
Roles¶
Role |
Who |
Responsibility |
|---|---|---|
Requester |
Tenant / tenant admin |
Creates a license request against an |
External licensing authority |
The integrator |
Polls for pending requests and returns approve / refuse / request-information decisions. This is the role you implement. |
Bazaar |
Platform |
Routes the request, records the decision, issues the signed license grant on approval, and exposes reconciliation feeds. |
The external authority is a machine actor. It authenticates with a DotID service-user access key (AK/SK), not a human login.
Lifecycle¶
request created ──▶ PENDING ──▶ APPROVED ──▶ (Bazaar issues license grant)
│
├──▶ REFUSED
└──▶ INFORMATION_REQUIRED ──▶ (requester supplies more) ──▶ PENDING
After issuance, an approved grant can be compensated (revoked or superseded) by the same external authority — for example when a downstream contract lapses.
Authentication — DotID AK/SK (FGAI-HMAC-SHA256)¶
Requests are signed with a DotID service user’s access key. Bazaar never sees your secret key; it forwards each signature to DotID for verification and derives an account-scoped service principal from the result.
Requirements for the key:
It must belong to a service user (machine principal), not a human user.
It must be scoped to a single account — the account that owns the license requests you will decide. A key can only see and decide requests in its own account.
It must be issued with a credential scope of
bazaarorsoftware-licensing. A key with any other scope is rejected.
Signing follows the FGAI-HMAC-SHA256 scheme (a SigV4-shaped derived-key
signature). Each request carries:
Authorization: FGAI-HMAC-SHA256 Credential=<accessKeyId>/<date>/<region>/<service>/fgai4_request, SignedHeaders=host;x-fgai-content-sha256;x-fgai-date, Signature=<hex>X-FGAI-Date: <YYYYMMDDTHHMMSSZ>X-FGAI-Content-SHA256: <sha256-hex of the request body>Host: api.flexgalaxy.ai
A reference signer is provided; you do not compute the derived-key chain by hand.
API surface¶
All paths are product-neutral and served from https://api.flexgalaxy.ai.
Purpose |
Call |
|---|---|
Poll your work list |
|
Read other states |
|
Submit a decision |
|
Delegate an offer’s approval |
|
Change feed (reconcile) |
|
Licensed-subject roster |
|
Register a webhook |
|
Compensate a grant |
|
Conformance sandbox |
|
GET …/requests returns your work list: the requests you can still decide —
PENDING, INFORMATION_REQUIRED, and the APPROVED device requests still inside
the override window. Requests nobody has submitted are not on it, because a
DRAFT permits no decision and answers 409 decision_not_allowed. Pass state
with one name, or several separated by commas, to read any other part of your
book; unknown names are rejected rather than quietly matching nothing.
Submitting a decision¶
POST /external-software-licensing/v1/requests/{requestId}/decisions requires an
Idempotency-Key header (safe to retry) and a body:
{
"decision": "APPROVE",
"expectedRevision": 1,
"externalReference": "WHITE-CASE-4711",
"reasonCode": "APPROVED_BY_POLICY",
"safeExplanation": "License approved per contract terms.",
"quantity": 3,
"validityDays": 90
}
decisionis one ofAPPROVE,REFUSE,REQUEST_INFORMATION(→ request stateAPPROVED,REFUSED,INFORMATION_REQUIRED).expectedRevisionis the request’s currentrevision, read from the poll response — optimistic concurrency. A stale value returns409(revision conflict); re-read and retry.For a refusal, send
"decision": "REFUSE"with areasonCodeand asafeExplanation(user-safe text). The term fields are for an approval only and are rejected with400on aREFUSEor aREQUEST_INFORMATION, which grant nothing.
Approving different terms than were asked for. An approval may narrow a request, and the narrowed terms are what the platform issues:
quantity— the seats you approve. Between1and the quantity the request asked for; outside that range is400. Omit it to approve the full request. The issued grant carries this number, and it is the number the customer can assign: an approved 3 of 5 yields exactly three assignable seats, and the fourth assignment is refused.validityDays— the window you approve,1–3650. Omit it for the platform default of 365 days. The issued grant’svalidTo, and thevalidToclaim inside the signed licence certificate, are computed from this value.
Both are also recorded with the decision as your evidence of the terms you approved and are readable back on the request’s ledger.
scope and constraints are not accepted. Sending either is rejected with
400 decision_term_not_supported, naming the field. Issuance has no defined
behaviour for them — there is no rule for what narrowing a scope would mean and
nothing that enforces one — so rather than accept a term and ignore it, this API
refuses it. If your licensing policy needs a term these fields would carry, refuse
the request with a reasonCode and have it re-filed on terms you can grant.
externalReferenceis unique per account. Re-using one for a different decision is refused with409 external_reference_conflict— which is what makes it a safe key to look a case up by in both systems.
Response:
{ "decisionId": "…", "eventId": "…", "state": "APPROVED", "revision": 2, "issuanceState": "…" }
Whose account a request lands in¶
A request names its subject — for a robot, subjectFrn is that robot. The account
the request is held by is not taken from whoever filed it: the platform resolves
the robot’s owner from its own device inventory at request time, and the request, the
resulting license and the bill all land in that owner’s account.
Two consequences for your integration:
ownerAccountIdon a row is the tenant, not the requester. A head office, a partner, or a service organization with delegated authority can file the request; the owner you address about that robot is still the account on the row.An unknown or unowned robot is refused, not guessed at. There is no fallback to the caller’s own account for a robot the platform cannot resolve an owner for.
Requests filed by the robot itself¶
A robot can request a license for its own identity. You will see these on your work list like any other request, with two differences worth handling:
They arrive already
PENDING. There is noDRAFTstep, because no person is going to press submit, so a self-request is decidable the moment you first see it.Nobody filed it on a human’s behalf. The subject is derived by the platform from the device, never taken from the request body, so the subject on the row is an attested fact rather than a claim to check.
Everything else is identical: a self-request is an ordinary approval-gated request. It is never auto-approved, and it carries the same states, the same revision concurrency, and the same idempotency rules.
Delegating approval to the owner¶
You can hand the approve/refuse decision for one of your offers back to the customer — their own administrators then decide in the console, with no round trip to you — while remaining the authority of record.
POST /external-software-licensing/v1/offers/{offerId}/approval-delegation
{ "ownerAccountId": "acct-4711", "delegated": true }
or, scoped to a branch of the customer’s organization rather than one account:
{ "organizationId": "…", "subtreeOuId": "…", "delegated": true }
Field |
Meaning |
|---|---|
|
delegate for one owner account. Omit it (and the organization fields) to delegate for every account that takes this offer. |
|
delegate for an organizational unit and everything beneath it. Accounts added to that branch later inherit the delegation. |
|
|
Send either ownerAccountId or both organization fields — mixing the two, or
sending only one of the pair, is 400 approval_delegation_scope_invalid. Setting a
delegation twice is idempotent and returns the existing one.
Response:
{ "delegationId": "…", "offerId": "…", "scopeType": "ORGANIZATION_SUBTREE",
"ownerAccountId": null, "organizationId": "…", "subtreeOuId": "…",
"delegated": true, "lifecycleVersion": 2 }
Only the offer’s external authority may delegate it — the publisher of the
software cannot, and neither can anyone else. An offer you are not the authority for
answers 404 software_offer_not_found rather than 403, so the call cannot be used
to discover that an offer exists.
You keep the last word¶
Delegating is not giving the decision away:
Delegated requests stay on your work list. They carry
"decisionAuthority": "BAZAAR"(a direct one carries"EXTERNAL"), so you can tell the two apart. Deciding a delegated request overrides the owner, and is recorded as an override.A robot-held license has a veto window. After the owner approves, you may still refuse —
APPROVED→REFUSE— for as long as issuance has not begun. Once the request reaches issuing, the window has closed and the refusal is409. That race is settled by the sameexpectedRevisioncheck as every other decision.Revoking a delegation fails in-flight work closed. Send
"delegated": false, and the next decision or issuance on a request that was routed under it is refused instead of quietly proceeding. The same happens if the owner account leaves the delegated branch of the organization. The delegation record is kept either way, so the provenance of an already-decided request survives.Membership has to be resolvable. An organization-subtree delegation is evaluated against live organization membership. If that cannot be read, the request fails closed with
503 approval_delegation_membership_unavailable— retry it rather than treating it as “not delegated”.
Licensed-subject roster¶
GET /external-software-licensing/v1/metering-subjects lists the subjects —
robots — currently under your license authority. Use it to answer “which
terminals am I the licensor for right now”, and to reconcile against your own
records.
A subject is on the roster while its license actually confers authority. When a grant is revoked, or a seat is unassigned, the subject leaves. Reconciliation is therefore a set difference: anything in your records that is not on the roster is no longer licensed by you.
Each row:
{
"cursor": "1408:",
"subjectFrn": "frn:acct-4711:thingmake:thing/robot-0912",
"ownerAccountId": "acct-4711",
"offerId": "…",
"licenseRequestId": "…",
"licenseState": "ISSUED",
"licenseHolder": "DEVICE",
"grantId": "…",
"grantState": "ACTIVE",
"assignmentId": null,
"licensedQuantity": 1,
"validFrom": "…",
"validTo": "…"
}
ownerAccountIdis the tenant that owns the robot — not your account and not the publisher’s. It is the account you address for anything about that robot.licenseHolderisDEVICEorACCOUNT. ADEVICE-held license is borne by the robot itself, so it contributes exactly one row andassignmentIdisnull. AnACCOUNT-held license is a pool of seats the tenant assigns, so it contributes one row per assigned terminal, each with its ownassignmentId.licensedQuantityis the licensed entitlement (seats), not a measurement.
This endpoint returns no usage. Bazaar knows what is licensed, not what a robot did. Usage counters are a separate capability that each device owner grants you directly and can revoke at any time, without affecting this roster.
Reconciling the roster against usage¶
The roster is the denominator of a metering run: it answers “which robots am I the licensor for right now”. The counters are read from the device platform’s own usage-metering capability, which each device owner grants you per scope and can revoke independently — see App-scoped telemetry and usage metering in the FlexGalaxy.AI device-platform developer documentation for the counters, their buckets, and the grant an owner writes for you.
Reconcile in that order, and the failure modes stay distinguishable:
Walk the roster to
hasMore: falseand take the set ofsubjectFrnvalues.Read usage per owner scope for the robots in that set.
A robot on the roster with no counters is licensed but not being measured — either it has not reported, or that owner has not granted you the usage capability. It is not an unlicensed robot.
A robot with counters but not on the roster is one your license no longer covers. Stop billing for it; the change feed tells you when and why it left.
The two halves are deliberately independent: an owner revoking measurement must not look like a license lapsing, and a license lapsing must not look like a quiet fleet.
Paging the roster¶
after is an opaque cursor string taken from nextCursor — not the plain
number used by …/requests and …/changes. One license request can contribute
many subjects, so a row needs more than a single number to address it. Pass the
previous response’s nextCursor verbatim.
GET …/metering-subjects?limit=50
GET …/metering-subjects?after=<nextCursor>&limit=50
Keep paging while hasMore is true. limit defaults to 50 and is capped at
100. A cursor that is not a value we handed you is rejected with 400 rather
than silently restarting — so a partial read can never be mistaken for a
complete one. Because the roster is what you make absence decisions from,
always walk to hasMore: false before concluding a subject is gone.
Reconciliation¶
Two mechanisms keep an external authority consistent with Bazaar:
Change feed —
GET …/changes?cursor=<cursor>returns an ordered, cursor-paged stream of request/decision changes. Persist the cursor and resume from it; this is the source of truth for catch-up after downtime.Webhooks — register an HTTPS callback to be notified of new pending requests. Webhooks are an optimization; the change feed remains authoritative, so a missed webhook is always recoverable by replaying the feed.
Conformance sandbox¶
Before going live, exercise the integration against
POST …/sandbox/scenarios/{scenario} with scenarios covering the paths you must
handle: SUCCESS, REFUSAL, CONCURRENCY (revision conflict), RETRY
(idempotent replay), MISSED_WEBHOOK, and ISSUANCE_FAILURE.
Integration checklist¶
Obtain an account-scoped service user and an AK/SK for it, minted with credential scope
bazaarorsoftware-licensing.Ensure the service user holds the
bazaar:ExternalLicenseRequest:{Read,Decide, Compensate}andbazaar:ExternalLicenseWebhook:Managepermissions in its account.Implement the signer for
FGAI-HMAC-SHA256.Poll
GET …/requests, or register a webhook.For each request,
POST …/decisionswith the matchingexpectedRevisionand anIdempotency-Key.Consume
GET …/changesto reconcile, and handle409by re-reading revision.Handle requests that arrive already
PENDINGwith no human requester — a robot asking for its own license — and address the owner named on the row, not the caller who filed it.If you delegate approval to customers, add
bazaar:ExternalLicenseDelegation:Manage, and keep polling: a delegated request still reaches your work list with"decisionAuthority": "BAZAAR", and overriding it is your call to make.If you meter licensed robots, add
bazaar:ExternalLicenseMeteringSubject:Readand walkGET …/metering-subjectstohasMore: false.Validate every path against the conformance sandbox before production.