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.
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 param | Description |
|---|---|
start | Start 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. |
end | End date (inclusive), YYYY-MM-DD. Defaults to the active credit window's end, or today when no active window is available. |
end_user_id | Filter to a single end user. |
feature_type | Filter to a single feature type. |
limit | Max breakdown rows to return (1–1000). Defaults to 100. |
offset | Number of breakdown rows to skip (pagination). Defaults to 0. |
sort | Field the breakdown is ordered by: credits, end_user_id, or feature_type. Defaults to credits. |
order | Sort 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 }
]
}
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.
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
usertoImageEditor.createEditor(). - Server-side AI endpoints — send
end_user_idin the request body ofPOST /v3/templates/generateandPOST /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:
- Verify and deduplicate webhooks as described below, then use them for fast updates.
- Reconcile completed UTC days from this endpoint.
- 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.
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"
}
| Field | Description |
|---|---|
exhaustion_behavior | "disable" hides AI features when credits run out; "show_error" shows an error prompt. |
threshold_alerts | Usage percentages (1–100) at which a threshold_reached webhook fires for capped usage. |
webhook_url | HTTPS endpoint that receives AI credit webhooks. |
All fields are optional; omitted fields keep their current values.
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
}
| Field | Description |
|---|---|
exhaustion_behavior | "disable" or "show_error". |
threshold_alerts | Configured usage percentages that fire threshold_reached. |
webhook_url | The configured HTTPS endpoint, or null. |
has_signing_secret | Whether 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.
| Event | Fires when… |
|---|---|
ai.credits.usage_recorded | After each classified real-time AI call. |
ai.credits.threshold_reached | Capped usage crosses a configured percentage. |
ai.credits.exhausted | A 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"
}
}
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:
| Header | Value |
|---|---|
X-Unlayer-Signature | sha256=<hex> HMAC of the request. |
X-Unlayer-Timestamp | Unix milliseconds when the request was signed. |
X-Unlayer-Event | The event name. |
X-Unlayer-Delivery-Id | Stable 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 param | Description |
|---|---|
status | Filter to a single status: pending / delivered / failed. |
event | Filter to a single event. |
limit | Max deliveries to return (1–100). Defaults to 50. |
offset | Number 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 param | Description |
|---|---|
limit | Max attempts to return (1–100). Defaults to 50. |
offset | Number 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.
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_type | Meaning |
|---|---|
full_template_gen | Generating a full template / layout. |
block_edit | Editing a block or generating text suggestions. |
html_import | Importing a design from HTML. |
image_import | Importing an image. |
image_generation | Generating a new image from a prompt. |
image_edit | Editing 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 newAI 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.
Related
- Authentication — how to create the API Key these endpoints require.
- Cloud API overview and the API reference.
- End-User Identification — pass
endUserIdso usage is attributed per end user. - In-Builder Credit Upsell — display a host-managed per-user allowance and purchase flow.
- AI Assistant Events & Callbacks — session telemetry that complements, but does not replace, billing usage.
- AI Assistant — the feature these credits pay for.