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 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.

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.

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, 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.

Customize credit notice text

The host-managed notice uses the following translation override keys. These are separate from the workspace exhaustion messages.

KeyUsed for
labels.credits.default_unitDefault display unit when no custom unitLabel is supplied.
labels.credits.low_balanceLow-balance message; receives {remaining} and {unitLabel}.
labels.credits.exhaustedExhausted-allowance message; receives {unitLabel}.
labels.credits.disabledHost-disabled message; receives {unitLabel}.
labels.credits.invalid_configurationInvalid or incomplete allowance snapshot message.
labels.credits.exhausted_titleExhausted blocking-state title; receives {unitLabel}.
labels.credits.unavailable_titleDisabled or invalid blocking-state title; receives {unitLabel}.
labels.credits.purchase_actionPurchase button label.
labels.credits.purchase_failedFallback 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:

  1. Identify the end user with unlayer.init({ user }).
  2. Verify signed ai.credits.usage_recorded webhooks, then use X-Unlayer-Delivery-Id for 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.
  3. Reconcile completed UTC days from the AI credit usage breakdown.
  4. 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.
  5. Pass the current snapshot through credits. Send an exhausted or disabled snapshot when the user reaches your limit, then refresh it with setCredits after 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 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.