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 across AI Assistant, Image Editor AI editing,
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.
The same allowance applies to the Image Editor. setCredits updates it while
open; blocked AI credits leave Crop, Resize, Filter, and other non-AI tools
available.
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.
Customize credit notice text
The host-managed notice uses the following translation override keys. These are separate from the workspace exhaustion messages.
| Key | Used for |
|---|---|
labels.credits.default_unit | Default display unit when no custom unitLabel is supplied. |
labels.credits.low_balance | Low-balance message; receives {remaining} and {unitLabel}. |
labels.credits.exhausted | Exhausted-allowance message; receives {unitLabel}. |
labels.credits.disabled | Host-disabled message; receives {unitLabel}. |
labels.credits.invalid_configuration | Invalid or incomplete allowance snapshot message. |
labels.credits.exhausted_title | Exhausted blocking-state title; receives {unitLabel}. |
labels.credits.unavailable_title | Disabled or invalid blocking-state title; receives {unitLabel}. |
labels.credits.purchase_action | Purchase button label. |
labels.credits.purchase_failed | Fallback purchase error when the callback supplies no custom message. |
For example, add these overrides to the same unlayer.init() call as your
credits configuration:
unlayer.init({
locale: 'en-US',
translations: {
'en-US': {
'labels.credits.default_unit': 'AI credits',
'labels.credits.low_balance': '{remaining} {unitLabel} left.',
'labels.credits.exhausted': 'Add more {unitLabel} to keep creating.',
'labels.credits.purchase_action': 'Buy more',
},
},
});
Keep {unitLabel} and {remaining} where you want those values inserted.
To use the translated default unit, omit credits.ai.unitLabel. If you supply
a custom unitLabel, localize that string in your application. Likewise, a
custom message returned by credits:purchase is displayed as supplied;
localize it before returning it.
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 }). - Verify signed
ai.credits.usage_recordedwebhooks, then useX-Unlayer-Delivery-Idfor ordinary retry deduplication and update your backend ledger for a fast response. The header is unsigned and not replay-proof; see signature and duplicate handling. - Reconcile completed UTC days from the AI credit usage breakdown.
- Apply each end user's allowance and reset dates in your backend. Unlayer's native reset is workspace-wide; custom per-user cycles belong to your application.
- Pass the current snapshot through
credits. Send an exhausted or disabled snapshot when the user reaches your limit, then refresh it withsetCreditsafter usage, a reset, or a purchase.
Unlayer does not call your backend to approve each Assistant prompt. The
credits snapshot is a UI gate, not a tamper-proof per-user limit. Keep the
commercial ledger in your backend; Unlayer's workspace allowance is the native
server-side enforcement boundary.
The usage endpoint accepts start, end, and end_user_id, so your daily
reconciliation can match each user's commercial cycle. Reconcile settled days
rather than repeatedly replacing the current UTC day's provisional total.
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.