Skip to main content
Version: Latest

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.

AI Assistant open in the Unlayer editor with a low-balance notice showing 20 credits remaining and a Get more buttonAI Assistant open in the Unlayer editor with a low-balance notice showing 20 credits remaining and a Get more button

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.

FieldTypeDescription
limitnumber | nullTotal host allowance. Use null for an explicitly uncapped end user.
usednumberAmount of the host allowance already consumed.
disabledbooleanOptional host override that blocks the resource regardless of the numeric balance.
lowBalanceThresholdPercentagenumberOptional consumed percentage for the low-balance notice, from 0 to 100. Defaults to 80.
unitLabelstringOptional white-label display unit, such as credits or messages. Defaults to credits.
purchaseUrlstringOptional 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, or disabled.
  • 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:

  1. Identify the end user with unlayer.init({ user }).
  2. Read per-end-user consumption from the AI credit usage breakdown and maintain the commercial allowance in your own backend.
  3. Pass that backend's current snapshot through credits.
  4. Enforce the same allowance in your backend so a user cannot bypass the builder UI.
  5. 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 scriptBuilder versionBehavior
CurrentCurrentcredits, credits:purchase, and setCredits are available.
CurrentOlder version-locked builderThe older builder ignores the extra init field and message. Its existing workspace enforcement remains active.
OlderCurrentNo 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.