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.

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

reset_date is when the current credit period resets, or null when there is no active billing period — including once a subscription is cancelled, deactivated, or its term has ended (the balance has no upcoming reset). For a subscription set to cancel at the end of its term, reset_date still shows the term end until then; the balance expires on that date rather than resetting.

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 updated near real time and grouped by the UTC date when the AI activity occurred. Recent activity may take a short time to appear.

Query paramDescription
startStart date (inclusive), YYYY-MM-DD. Must be on or before end. Defaults to the current period.
endEnd date (inclusive), YYYY-MM-DD. Defaults to the current period.
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.

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 only when you identify the end user via unlayer.init({ user }). Calls made without an identified end user are reported with end_user_id: null.

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

EventFires when…
ai.credits.usage_recordedAfter every AI call.
ai.credits.threshold_reachedUsage crosses a configured percentage.
ai.credits.exhaustedThe workspace balance reaches 0 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"
}
}

ai.credits.threshold_reached

Fires once per configured threshold per billing period. 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 the workspace pool reaches 0. 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 this delivery, identical on every attempt.

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.

Treat X-Unlayer-Delivery-Id as an idempotency key. It stays the same across every attempt of the same delivery, so record the ids you have processed and ignore one you have seen before:

if (await alreadyProcessed(headers['x-unlayer-delivery-id'])) {
return res.sendStatus(200); // already handled — acknowledge and stop
}

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 expected =
'sha256=' +
crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const received = headers['x-unlayer-signature'];
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.

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 text or a single block.
html_importImporting a design from HTML.
image_importImporting an image.
image_generationGenerating or editing an image.