In-Builder Credit Upsell
Use the credits configuration to show the current end user's host-managed
allowance inside the builder. The builder can warn when the allowance is low,
block the relevant action when it is exhausted, and open your purchase flow
without sending the end user away.

This is a UI and client-side preflight contract. Your application owns the end-user ledger, pricing, resets, purchases, and backend enforcement. Unlayer continues to meter and enforce the workspace's total allowance separately. A value supplied here cannot increase the workspace's server-side entitlement. It also cannot secure a generation endpoint by itself: treat the browser state as a soft UI gate and enforce the same end-user allowance in your backend.
Configure an end-user allowance
Pass a complete snapshot for the current end user's AI allowance:
unlayer.init({
id: 'editor',
projectId: 1234,
user: { id: 'customer-42' },
credits: {
ai: {
limit: 100,
used: 80,
disabled: false,
lowBalanceThresholdPercentage: 80,
unitLabel: 'credits',
purchaseUrl: 'https://app.example.com/billing/ai',
},
},
});
Use the single ai allowance for consumption across AI Assistant and AI
Template Importer. The display hooks run inside the builder; a direct
POST /v3/templates/import call does not receive browser-side unlayer.init()
configuration. Enforce the same end-user allowance in your backend before
calling the import API, then refresh any open builder with setCredits.
| Field | Type | Description |
|---|---|---|
limit | number | null | Total host allowance. Use null for an explicitly uncapped end user. |
used | number | Amount of the host allowance already consumed. |
disabled | boolean | Optional host override that blocks the resource regardless of the numeric balance. |
lowBalanceThresholdPercentage | number | Optional consumed percentage for the low-balance notice, from 0 to 100. Defaults to 80. |
unitLabel | string | Optional white-label display unit, such as credits or messages. Defaults to credits. |
purchaseUrl | string | Optional absolute HTTP(S) link-out fallback when no purchase callback is registered. |
limit is the total allowance, not the remaining balance. For example,
limit: 100 and used: 80 means 20 remain.
When an AI Assistant conversation is empty, an exhausted allowance displays a
full blocking state. If the allowance becomes exhausted after the end user has
started a conversation, the builder keeps the conversation visible, displays
the exhausted notice above it, and disables further prompts. A successful
purchase result or permissible setCredits update removes the notice and
re-enables the prompt without clearing the conversation.
Omit a resource when you do not maintain an end-user allowance for it. The builder then falls back to Unlayer's server-side workspace enforcement; it does not grant unlimited access. If a resource is present but its required fields are missing or invalid, the builder fails closed and blocks it until you send a valid snapshot.
When host-managed AI credits are present, that end-user snapshot is the builder's display source of truth and the workspace balance meter is hidden. The workspace balance is shown as the fallback when host-managed credits are omitted. Unlayer's backend still enforces the workspace balance in both cases.
Keep the purchase inside the builder
Register credits:purchase to open your own checkout modal or purchase UI.
The recommended form returns a result, so an asynchronous checkout can remain
open for as long as payment requires:
unlayer.registerCallback('credits:purchase', async function (request) {
try {
const result = await openCreditCheckout({
idempotencyKey: request.requestId,
resource: request.resource,
reason: request.reason,
});
if (!result.purchased) {
return { status: 'cancelled' };
}
const freshCredits = await getEndUserCredits(request.resource);
return {
status: 'success',
credits: freshCredits,
};
} catch (error) {
return {
status: 'error',
message: 'We could not complete the purchase. Please try again.',
};
}
});
The request contains:
requestId: a unique purchase-attempt ID suitable for use as an idempotency key.resource:ai.reason:low_balance,exhausted, ordisabled.credits: the snapshot currently rendered by the builder.
Complete the request exactly once. Either return (or resolve with) one of these
results, as above, or use the callback form and pass the result to done:
unlayer.registerCallback('credits:purchase', function (request, done) {
openCreditCheckout({ idempotencyKey: request.requestId })
.then((result) => {
done(
result.purchased
? { status: 'success', credits: result.credits }
: { status: 'cancelled' },
);
})
.catch(() => done({ status: 'error' }));
});
Do not both return a result and call done. The first completion wins.
{ status: 'success', credits }replaces the current snapshot and resumes the resource when the new state permits it.{ status: 'cancelled' }closes the attempt quietly and leaves the current state unchanged.{ status: 'error', message? }shows a clear error and leaves the end user blocked.
The builder treats a thrown handler, a rejected handler promise, or a malformed
success snapshot as an error. There is deliberately no client-side payment
timeout: bank authentication and hosted checkout can take several minutes, and
automatically enabling a retry could charge the same end user twice. The CTA
remains busy until the handler completes. Use requestId as an idempotency key
and ensure every handler path eventually completes. The pending operation
belongs to the editor session rather than the currently visible panel, so
closing and reopening the AI UI does not enable a duplicate purchase while
checkout is still running.
Persist payment and allowance changes in your backend, not in the callback.
After a reload or reconnect, initialize the builder from that durable state. If
your application refreshes the displayed snapshot with setCredits while a
purchase is pending, the original purchase remains active and the CTA stays
disabled. Complete that handler by returning, resolving, or calling done;
only the completion associated with its matching requestId ends the pending
purchase.
When both a handler and purchaseUrl are configured, the handler takes
precedence. If only purchaseUrl is present, the CTA opens it in a new tab. If
neither is configured, the status remains visible without a purchase CTA.
Non-HTTP(S), relative, and malformed purchase URLs are ignored.
Refresh credits at runtime
Replace one resource's complete snapshot after a purchase, plan change, reset, or any other host-side update:
unlayer.setCredits('ai', {
limit: 200,
used: 80,
disabled: false,
lowBalanceThresholdPercentage: 80,
unitLabel: 'credits',
purchaseUrl: 'https://app.example.com/billing/ai',
});
setCredits replaces the complete snapshot; it does not merge fields from the
previous one. Include every optional display or purchase setting you still want
to use.
The builder does not increment used itself because it cannot know how your
application prices each operation. Refresh the snapshot from your backend as
usage changes. Do not put raw provider token counts, model names, or costs into
this display contract.
Worked example: the Benchmark Email pattern
Benchmark Email already follows the ownership model: it uses Unlayer's usage API as the data source while keeping end-user billing and allowances in its own application. The complete pattern, including these builder hooks, is:
- Identify the end user with
unlayer.init({ user }). - Read per-end-user consumption from the AI credit usage breakdown and maintain the commercial allowance in your own backend.
- Pass that backend's current snapshot through
credits. - Enforce the same allowance in your backend so a user cannot bypass the builder UI.
- After usage or purchase, return or push a fresh snapshot with
setCredits.
Never rely on the browser snapshot as authorization. An end user can modify client-side state; only your backend and Unlayer's workspace-level enforcement are authoritative.
Version compatibility
The credit upsell contract is additive:
| Host script | Builder version | Behavior |
|---|---|---|
| Current | Current | credits, credits:purchase, and setCredits are available. |
| Current | Older version-locked builder | The older builder ignores the extra init field and message. Its existing workspace enforcement remains active. |
| Older | Current | No host snapshot is supplied, so the builder falls back to workspace enforcement. |
Do not assume setCredits is available when intentionally loading an older,
version-locked builder. Upgrade both the host script and builder version before
depending on the in-builder purchase flow.