Skip to content

Public pricing

GET /v1/public/{tenantId}/{productId}/pricing

Every purchasable key type on one app, with its price, term and device cap. Public — no auth, so it can be called straight from your marketing site’s JavaScript.

It exists so you never have to hardcode a price. Write $29 into your pricing page and the day you change it in Keylight your site quotes one number while Stripe charges another. Read it from here instead and the page follows on the next load.

Terminal window
curl https://api.keylight.dev/v1/public/acme/myapp/pricing
{
"tenantId": "acme",
"product": { "productId": "myapp", "displayName": "My App" },
"keyTypes": [
{
"keyTypeId": "team",
"displayName": "Team",
"description": "Up to 5 devices per seat · Lifetime",
"priceInCents": 9900,
"priceCurrency": "usd",
"billingModel": "one_time",
"durationDays": null,
"activationLimit": 5,
"entitlements": ["pro-export"],
"scalesWithQuantity": true,
"maxSeats": 1000,
"buyUrl": "https://api.keylight.dev/buy/acme/myapp/team"
}
]
}
FieldNotes
descriptionThe same line the buyer reads under the product name at Stripe checkout.
priceInCentsPer seat when scalesWithQuantity is true, otherwise the whole price.
activationLimitDevices per seat when scalesWithQuantity is true.
scalesWithQuantityWhen true, append ?qty=N to buyUrl to sell N seats.
maxSeatsUpper bound qty is clamped to. 1 for a key type that does not scale.
buyUrlSend the buyer here. Never build the URL yourself.
const res = await fetch('https://api.keylight.dev/v1/public/acme/myapp/pricing');
const { keyTypes } = await res.json();
const team = keyTypes.find((k) => k.keyTypeId === 'team');
let seats = 1;
function render() {
price.textContent = `$${((team.priceInCents * seats) / 100).toFixed(2)}`;
caption.textContent = team.description;
buy.href = team.scalesWithQuantity ? `${team.buyUrl}?qty=${seats}` : team.buyUrl;
}

The seat stepper stays yours — your design, your page. Keylight supplies the numbers and takes the payment.

Only what a buyer can actually purchase is listed. Hand-issued key types, and any key type with no synced Stripe price, are omitted rather than advertised — a pricing page should never show a button that fails.

StatusWhen
404Unknown app, or nothing on it is purchasable. Also returned for a tenant that cannot currently sell — the endpoint never reports why.
429Rate limited (120 requests per minute per IP).

Responses are cacheable for 60 seconds and answer any origin. Handle a 404 by falling back to whatever your page renders when the fetch fails, rather than showing an empty pricing table.