Offers: pricing, approval, and where a license is held

A product is what you publish. An offer is the terms on which someone may take it. One product can carry several offers, and each one independently answers four questions: who may see it, what it costs, who approves it, and where the resulting license is held.

POST /software-publishing/v1/products/{productId}/offers
{
  "audience": "PUBLIC",
  "licenseModel": "SUBSCRIPTION",
  "approvalPolicy": "EXTERNAL",
  "externalAuthorityAccountId": "acct-4711",
  "priceMinor": 4900,
  "currency": "USD",
  "licenseHolder": "DEVICE"
}

Field

Values

audience

PUBLIC, or RESTRICTED with an audienceRef naming who it is for

licenseModel

FREE, PERPETUAL, SUBSCRIPTION, APPROVAL_REQUIRED

approvalPolicy

AUTO, PUBLISHER, EXTERNAL

externalAuthorityAccountId

required when approvalPolicy is EXTERNAL

priceMinor / currency

minor units, non-negative; currency defaults to USD

licenseHolder

DEVICE or ACCOUNT — optional; defaults from the product kind

These four axes are independent. licenseModel is the commercial shape, approvalPolicy is who decides, licenseHolder is where the license lives, and the product’s kind is what the artifact is. Choosing a subscription does not imply an approval; choosing an external authority does not imply a device-held license.

Where the license is held

This is the choice that changes what your customer manages every day.

ACCOUNT — the customer’s account holds a pool of seats. They subscribe once and assign seats to terminals, unassigning to recover capacity. This is the model for a web application, and for software licensed by headcount rather than by machine.

DEVICE — the robot holds the license. There is no seat pool at all:

  • One request and one decision per robot.

  • The signed license names the robot itself, so the license stays with that machine. There is nothing to assign, and an attempt to assign one is refused with 409 device_held_license_not_assignable.

  • Issuance also mints an X.509 certificate for that robot, bound to its identity and delivered over the device’s own channel. Your application validates it at start-up: the certificate, not a console flag, is what authorises the software to run.

  • Issuance and certificate delivery are one unit. If the certificate cannot be delivered — the owning account does not hold the product, or the listing has no current published version — issuance is refused with 409 device_certificate_delivery_unavailable and the request stays approved and retryable rather than becoming a license nothing can enforce.

If you omit licenseHolder, it is derived from what you published: firmware and applets default to DEVICE, everything else to ACCOUNT. State it explicitly when the default is not what you want.

Warning

audience, licenseModel and licenseHolder are fixed when the offer is created. A license snapshots its holder model at request time, so changing it under issued licenses would rewrite what was already granted. Offer edits cover the commercial fields only — price, currency, approval policy, and the external authority. To change the audience, the commercial model, or the holder, author a new offer and retire the old one.

Who approves

approvalPolicy

Decision maker

AUTO

nobody — a license is issued on request

PUBLISHER

decided inside the account that holds the request

EXTERNAL

the account named in externalAuthorityAccountId

EXTERNAL makes the licensing authority a distinct account from you: you publish, they decide. An offer that declares EXTERNAL without naming an authority account is rejected with 400, field externalAuthorityAccountId, and the same invariant is re-checked on every edit — you cannot clear the authority off a live external offer.

The integrator’s side of that relationship — polling, deciding, delegating back to the customer, compensating, and the licensed-subject roster — is documented in External software-licensing integration.

Going live

An authored offer starts in DRAFT: it exists but nobody can take it. Publish it explicitly:

POST /software-publishing/v1/offers/{offerId}/lifecycle
{ "action": "PUBLISH" }

PUBLISH moves DRAFT or SUSPENDED to ACTIVE, and requires the product itself to be published. A firmware offer additionally requires a release carrying signature evidence — an unsigned firmware offer is refused with 422 firmware_release_unsigned. SUSPEND takes an ACTIVE offer out of circulation; RETIRE is terminal, and a retired offer can no longer be edited.

From the CLI

fgai publisher offer-create --product <product-id> --body-file offer.json
fgai publisher offer-lifecycle --offer <offer-id> --body '{"action":"PUBLISH"}'
fgai publisher offer-list --product <product-id>