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.
{
"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 param | Description |
|---|---|
start | Start date (inclusive), YYYY-MM-DD. Must be on or before end. Defaults to the current period. |
end | End date (inclusive), YYYY-MM-DD. Defaults to the current period. |
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.
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 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"
}
| 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. |
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.
| Event | Fires when… |
|---|---|
ai.credits.usage_recorded | After every AI call. |
ai.credits.threshold_reached | Usage crosses a configured percentage. |
ai.credits.exhausted | The 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"
}
}
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 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 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 text or a single block. |
html_import | Importing a design from HTML. |
image_import | Importing an image. |
image_generation | Generating or editing an image. |
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. - AI Assistant — the feature these credits pay for.