Skip to main content

AI Credits API & Webhooks

If you embed the Unlayer builder and resell the AI Assistant to your own end-users, these endpoints give you programmatic visibility into AI credit consumption so you can build billing, set limits, and message your users.

Everything here is measured in credits — the single unit exposed across every endpoint and webhook.

Credits are pooled per workspace

A project's AI credits belong to its workspace. Every project in the same workspace draws from — and reports against — one shared balance. Webhook and alert settings, however, are configured per project.

All endpoints require an API Key (unlayer_sk_*) scoped to the project, sent as a bearer token. See Authentication. A key can only access its own project.

If you call these endpoints with a Personal Access Token instead, your account must be an admin or owner of the project — these endpoints expose the webhook signing secret and destination URL. Other roles receive 403.


Endpoints​

Get credit balance​

GET /v3/projects/:id/ai-credits

Returns a near-real-time snapshot of the current workspace credit pool. The project ID identifies the workspace and authorizes the request; it does not limit the balance to that project. Calling this endpoint for two projects in the same workspace returns the same balance.

{
"is_capped": true,
"credits_total": 10000,
"credits_used": 7400,
"credits_remaining": 2600,
"reset_date": "2026-08-01T00:00:00.000Z"
}

is_capped reports whether enforcement can constrain AI usage, not whether Console has a positive allowance to display. A shadow-mode workspace with a plan or add-on allowance can show a remaining allowance in Console while this endpoint returns false; the editor balance indicator stays hidden and threshold/exhausted webhooks do not fire.

When is_capped is false, usage is metered for visibility but remains uncapped: credits_total and credits_remaining describe the metered allocation, not an available balance, and reaching zero does not pause AI or create overage charges. Use credits_used to report consumption, but do not render an exhausted state from credits_remaining.

reset_date is the end timestamp returned for the current credit window, or null when the service cannot resolve an active future billing window. That includes a subscription that has been cancelled, deactivated, or reached the end of its term.

A scheduled cancellation does not make reset_date null while the current window is still active. If the subscription ends with that window, credits expire on that date rather than renewing. Annual subscriptions can still have monthly credit resets before their final term end, so do not interpret every reset_date as a subscription renewal or cancellation date.

AI credit balances update asynchronously after usage is recorded. Re-delivery of the same usage event does not consume additional credits. Current-day activity is counted by the live usage processor; reconciliation finalizes previous UTC days without adding the same activity again. A delayed or failed live update may remain absent until the next day's reconciliation.

Get usage breakdown​

GET /v3/projects/:id/ai-credits/usage

Returns the selected project's credit consumption, broken down by end user and feature type. Usage is grouped by the UTC date when the AI activity occurred. The current UTC day is updated near real time and may take a short time to appear. Each recorded AI event contributes once to this breakdown, including when its delivery is retried.

Query paramDescription
startStart date (inclusive), YYYY-MM-DD. Must be on or before end. Defaults to the active credit window's start, or 30 UTC dates before today when no active window is available.
endEnd date (inclusive), YYYY-MM-DD. Defaults to the active credit window's end, or today when no active window is available.
end_user_idFilter to a single end user.
feature_typeFilter to a single feature type.
limitMax breakdown rows to return (1–1000). Defaults to 100.
offsetNumber of breakdown rows to skip (pagination). Defaults to 0.
sortField the breakdown is ordered by: credits, end_user_id, or feature_type. Defaults to credits.
orderSort direction: asc or desc. Defaults to desc.
{
"total_credits_used": 6300,
"total": 3,
"breakdown": [
{ "end_user_id": "user_123", "feature_type": "block_edit", "credits": 200 },
{
"end_user_id": "user_456",
"feature_type": "full_template_gen",
"credits": 4000
},
{ "end_user_id": null, "feature_type": "image_generation", "credits": 2100 }
]
}
Why the totals may differ

In the examples above, the workspace has used 7400 credits, while the selected project accounts for 6300. Usage from other projects in the workspace is included in credits_used but not in total_credits_used.

The totals may also differ briefly because the workspace balance and project breakdown are updated independently from the same asynchronous event stream. Even for a single-project workspace and an unfiltered current-period query, recent usage may reach one view before the other. Settled-day finalization keeps the detailed ledger aligned with the authoritative analytics data.

When neither date is supplied and the workspace has no active credit window, the fallback covers the 30 UTC dates before today plus today itself (31 calendar dates, inclusive).

The breakdown is paginated — one end_user_id × feature_type row per entry, up to limit. It defaults to highest-credit rows first; use sort / order to change that (e.g. sort=credits&order=asc for lowest first, or sort=end_user_id alphabetically). total is the number of breakdown rows matching the filter (across all pages), for paging. total_credits_used is the credit total across the entire filtered range, not just the returned page — so you can reconcile without walking every page. Narrow the response with end_user_id / a smaller date range, or page with limit / offset.

Per-user attribution

end_user_id is populated when the AI call identifies an end user:

  • Builder — identify the user via unlayer.init({ user }). The builder forwards this user to its built-in Image Editor automatically; no additional configuration is needed.
  • Standalone Image Editor — when you initialize it yourself, pass a user to ImageEditor.createEditor().
  • Server-side AI endpoints — send end_user_id in the request body of POST /v3/templates/generate and POST /v3/templates/import.

Calls made without an identified end user are reported with end_user_id: null and still consume credits. If you already identify your users and see unexpected unattributed usage, contact support so the originating requests can be investigated.

Settlement and reconciliation​

Treat the current UTC day's breakdown as provisional. After the day closes, Unlayer replaces the streaming projection with the complete settled result. That replacement can move a total up or down because the streaming and settled pipelines are separate views and the settled snapshot replaces the day rather than adding to it.

For reliable billing and limits:

  1. Verify and deduplicate webhooks as described below, then use them for fast updates.
  2. Reconcile completed UTC days from this endpoint.
  3. Compare a workspace balance only with the sum of all projects in that workspace over the same monthly window, with no end-user or feature filter.

The workspace balance remains the authority for whether more AI work can run. The settled usage endpoint is the durable source for project, end-user, and feature attribution.

Legacy activity

Billable legacy AI actions are included in this breakdown and the usage_recorded webhook. Identify the user in the builder configuration to attribute them automatically. Historical events without a recorded user ID remain unattributed. Calls made with your own supported provider key do not consume Unlayer AI credits; a stored key that is not used does not exempt a call made with Unlayer’s key.

Update settings​

PUT /v3/projects/:id/ai-credits/settings
{
"exhaustion_behavior": "show_error",
"threshold_alerts": [80, 95],
"webhook_url": "https://partner.com/webhooks/unlayer"
}
FieldDescription
exhaustion_behavior"disable" hides AI features when credits run out; "show_error" shows an error prompt.
threshold_alertsUsage percentages (1–100) at which a threshold_reached webhook fires for capped usage.
webhook_urlHTTPS endpoint that receives AI credit webhooks.

All fields are optional; omitted fields keep their current values.

Save your signing secret

The first time you set a webhook_url, the response includes a signing_secret — used to verify webhook signatures. It is returned once and never shown again. Store it securely.

{
"exhaustion_behavior": "show_error",
"threshold_alerts": [80, 95],
"webhook_url": "https://partner.com/webhooks/unlayer",
"has_signing_secret": true,
"signing_secret": "b1946ac9…"
}

Read settings​

GET /v3/projects/:id/ai-credits/settings

Returns the project's current configuration. The signing secret itself is never returned — only whether one exists.

{
"exhaustion_behavior": "show_error",
"threshold_alerts": [80, 95],
"webhook_url": "https://partner.com/webhooks/unlayer",
"has_signing_secret": true
}
FieldDescription
exhaustion_behavior"disable" or "show_error".
threshold_alertsConfigured usage percentages that fire threshold_reached.
webhook_urlThe configured HTTPS endpoint, or null.
has_signing_secretWhether a signing secret exists (the secret is never echoed).

Webhooks​

When a webhook_url is configured, Unlayer POSTs the following events to it. The setting applies to the whole project; it cannot be limited to selected end users. Filter data.end_user_id in your receiver, or query that end user from the usage endpoint.

EventFires when…
ai.credits.usage_recordedAfter each classified real-time AI call.
ai.credits.threshold_reachedCapped usage crosses a configured percentage.
ai.credits.exhaustedA capped workspace balance reaches zero credits.

Every request body has the shape { "event": "<name>", "data": { … } }.

ai.credits.usage_recorded​

{
"event": "ai.credits.usage_recorded",
"data": {
"project_id": 123,
"end_user_id": "user_123",
"feature_type": "block_edit",
"credits_deducted": 3,
"timestamp": "2026-07-01T12:00:00.000Z"
}
}

feature_type carries the same values as the usage endpoint, listed under Feature types. Handle an unrecognized one rather than switching exhaustively: image_edit is new, and the set can grow again.

Use this event for low-latency updates, not as the final billing total. The payload for an individual call is not revised after delivery, while the settled usage endpoint aggregates a complete day. Summing webhook credits_deducted values can therefore differ from the settled API total.

ai.credits.threshold_reached​

Fires once per configured threshold per billing period for a capped workspace. Uncapped usage does not emit threshold events. If the credit allocation increases mid-period (an add-on or plan change), the threshold re-arms and fires again the next time it's crossed against the larger allocation.

{
"event": "ai.credits.threshold_reached",
"data": {
"project_id": 123,
"threshold": 80,
"credits_remaining": 2000,
"credits_total": 10000,
"timestamp": "2026-07-01T12:00:00.000Z"
}
}

ai.credits.exhausted​

Fires once per billing period when a capped workspace pool reaches 0. Uncapped usage does not emit exhausted events because reaching the metered allocation does not pause AI. If the customer tops up mid-period (an add-on or plan change) and then exhausts the larger allocation, it fires again — so a top-up that runs out is not silently missed.

{
"event": "ai.credits.exhausted",
"data": {
"project_id": 123,
"workspace_id": 45,
"timestamp": "2026-07-01T12:00:00.000Z"
}
}
Delivery timing

usage_recorded is delivered in real time. threshold_reached and exhausted are derived from aggregated usage and may lag actual consumption by up to an hour.

Verifying signatures​

Each delivery carries four headers:

HeaderValue
X-Unlayer-Signaturesha256=<hex> HMAC of the request.
X-Unlayer-TimestampUnix milliseconds when the request was signed.
X-Unlayer-EventThe event name.
X-Unlayer-Delivery-IdStable id for tracing a delivery across attempts.

Handling duplicates​

Delivery is at-least-once: a failed delivery is retried automatically, and a delivery can occasionally be sent more than once even after it has succeeded — for example if a retry was already in flight when the first attempt landed.

Verify the signature and timestamp freshness before processing or deduplicating a request. Then use X-Unlayer-Delivery-Id to collapse ordinary at-least-once retries; it stays the same across attempts of one delivery:

const deliveryId = headers['x-unlayer-delivery-id'];

if (typeof deliveryId !== 'string' || !deliveryId) {
return res.sendStatus(400);
}

if (await alreadyProcessed(deliveryId)) {
return res.sendStatus(200); // already handled — acknowledge and stop
}

The signature authenticates the timestamp and raw body, but not X-Unlayer-Event or X-Unlayer-Delivery-Id. Use the event name from the verified body. The delivery id handles normal retry duplication; it is not replay-proof because it is unsigned. Keep the timestamp tolerance narrow, make downstream writes idempotent where your data model allows it, and use the settled usage endpoint—not webhook deduplication alone—as the billing authority.

Always respond 2xx to a duplicate. A non-2xx response marks the delivery failed and schedules another retry.

The signature is an HMAC-SHA256 over the string `${timestamp}.${rawBody}` using your signing_secret. Compute it yourself and compare in constant time:

import crypto from 'node:crypto';

function isValid(rawBody, headers, secret) {
const timestamp = headers['x-unlayer-timestamp'];
const received = headers['x-unlayer-signature'];
const signedAt = Number(timestamp);

if (
typeof timestamp !== 'string' ||
typeof received !== 'string' ||
!Number.isSafeInteger(signedAt) ||
Math.abs(Date.now() - signedAt) > 5 * 60 * 1000
) {
return false;
}

const expected =
'sha256=' +
crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');

return (
expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
);
}

Sign over the raw request body, before any JSON parsing — re-serializing can change bytes and break the comparison. The five-minute tolerance above is an example; choose a short window that fits your delivery latency and clock-skew requirements.

Delivery & retries​

Return a 2xx to acknowledge a webhook. A non-2xx response (or a timeout — endpoints have ~10s to respond) is retried. Each delivery is attempted up to 3 times in quick succession (~90s apart); if it still hasn't succeeded, it is re-attempted periodically for a while afterward, so a transient outage on your endpoint recovers on its own. Because a delivery can be retried, your endpoint must be idempotent — the same event may arrive more than once, and events are not guaranteed to arrive in order.

Delivery history & manual retry​

All events — usage_recorded, threshold_reached, and exhausted — are tracked in the delivery history. Inspect recent deliveries and re-send failed ones without waiting for the automatic retry.

List deliveries​

GET /v3/projects/:id/ai-credits/webhooks/deliveries

The delivery history, newest first.

Query paramDescription
statusFilter to a single status: pending / delivered / failed.
eventFilter to a single event.
limitMax deliveries to return (1–100). Defaults to 50.
offsetNumber of deliveries to skip (pagination). Defaults to 0.

Each delivery has event, status, attempts, last_status_code, end_user_id (when the event carries one), created_at, and delivered_at. The response also includes total — all deliveries matching the filter, ignoring paging.

List a delivery's attempts​

GET /v3/projects/:id/ai-credits/webhooks/deliveries/:deliveryId/attempts

The per-attempt history for one delivery, newest attempt first. Returns 404 if the delivery isn't found for this project.

Query paramDescription
limitMax attempts to return (1–100). Defaults to 50.
offsetNumber of attempts to skip (pagination). Defaults to 0.

Each attempt has attempt (the attempt number), status_code, error, and attempted_at, plus a total.

Retry a delivery​

POST /v3/projects/:id/ai-credits/webhooks/deliveries/:deliveryId/retry

Re-queue one delivery for another attempt. Returns 409 if it was already delivered. Retries deliver to your current webhook URL and signing secret, so correcting a wrong URL and retrying recovers the events that failed against the old one.

Retention

usage_recorded is high volume (one per AI call), so its rows are pruned from the history: successfully-delivered rows after 30 days, and failed or pending rows after 90 days (long past the last automatic retry) — filter with ?event=ai.credits.usage_recorded to find recent ones. threshold_reached and exhausted deliveries are retained. For a complete billing record, the usage breakdown endpoint remains the durable source of truth.

Rotating the signing secret​

POST /v3/projects/:id/ai-credits/settings/rotate-secret returns a new signing secret once. The previous secret stops working immediately, so update your verification before rotating. To check whether a secret is set without rotating, use Read settings.

A webhook_url is required — the signing secret is generated when you first set one. Rotating before then returns 400.


Feature types​

feature_type classifies what an AI call did:

feature_typeMeaning
full_template_genGenerating a full template / layout.
block_editEditing a block or generating text suggestions.
html_importImporting a design from HTML.
image_importImporting an image.
image_generationGenerating a new image from a prompt.
image_editEditing an image that already exists.

The last two are told apart by what the call did, not by where it came from. Today the builder's Magic Image generates and the Image Editor's AI chat edits, but either surface may do either over time, and the feature_type follows the operation.

Legacy Smart Headings, Smart Text, Smart Buttons, and image-to-text suggestions are included as block_edit. Legacy Magic Image generation and its image-prompt suggestions are included as image_generation. These actions already consumed credits; including them in the usage breakdown and usage webhooks does not add another charge. The builder's existing user identification supplies their end-user attribution automatically. Direct project-key calls to legacy text routes have no end-user identity and are reported as unattributed.

Older legacy events that are reprocessed can appear in these categories, but events recorded without an end-user ID remain unattributed. This change does not reconstruct missing identities or automatically backfill all past usage.

image_edit is new

AI image edits used to be reported as image_generation. They now have their own feature_type, so a filter or chart pinned to image_generation no longer includes them. If you break usage down by feature, add image_edit to whatever you already do for image_generation. Usage recorded before this change keeps its original image_generation classification; nothing is reclassified retroactively.