Skip to content
HTML/CSS to ImageDocs

Webhooks send an HTTP POST to your endpoint when something happens in your HCTI organization. Use them to publish content after an image reaches storage, keep your application in sync with template changes, or notify your team about usage.

Choose the events each endpoint receives, add headers or query parameters for your receiving service, and enable signatures to verify deliveries. The dashboard records event payloads and delivery attempts.

Open webhooks Webhooks API
  1. Open Webhooks in the dashboard and click Add webhook. Owners and Admins can manage webhooks. Users can view them and their delivery history.
  2. Enter a Name and Endpoint URL, such as https://hooks.example.com/hcti. Use a public HTTPS endpoint with a DNS hostname on the default HTTPS port. Add query parameters separately rather than including them in this URL.
  3. Under Events to receive, expand the groups and select the events your application needs. For example, select image_stored to act when rendered files have been saved.
  4. If your receiver needs authentication or routing values, click Add parameter, choose Header or Query, and enter its name and value. Mark credentials Secret to encrypt them and hide them from later views and delivery history.
  5. Turn on Sign deliveries to add signature verification. Clear Enabled and click Save so you can configure verification before the first delivery.
  6. Open Edit webhook, then Reveal signing secret, and copy it into your receiving application’s server-side configuration. The secret is generated for every webhook, including those created through the API, and is revealed only in the dashboard.
  7. Once your receiver is ready, select Enabled and Save. This queues a webhook_test delivery using your configured parameters and signing setting.

Creating an enabled webhook also queues a webhook_test delivery automatically. Your receiver should accept this event even if it only acts on a particular business event.

The webhook list also provides Send a test event, Edit webhook, View delivery history, and Delete webhook actions. Each enabled webhook accepts one test every 30 seconds. Your organization can make five manual test requests per minute.

Webhooks receive matching events while enabled. Adding a subscription does not replay earlier resource changes or already-stored images.

Free plans include up to 2 webhooks per organization. Paid plans include 10. Disabled webhooks still count toward that limit.

Use POST /v1/webhooks with webhooks:create_update and read access to the resources you subscribe to. For example, an image-storage subscription also needs images:read:

Terminal window
curl 'https://hcti.io/v1/webhooks' \
--user "$HCTI_API_ID:$HCTI_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"name": "Image publishing",
"url": "https://hooks.example.com/hcti",
"disabled": true,
"events": ["image_stored"],
"signing_enabled": true
}'

This creates a disabled webhook, so test_event_id is null and nothing is sent yet. Signed setup through REST or MCP includes a dashboard step: open Edit webhook → Reveal signing secret, then store the secret in your receiver’s server-side configuration. Signing secrets and secret parameter values are never returned by REST or MCP.

Once your receiver is ready, enable the webhook in the dashboard, or send the complete configuration to POST /v1/webhooks/WEBHOOK_ID with disabled: false. Keep signing_enabled: true and include every subscription and parameter you want to retain. Updates replace the configuration rather than patching individual fields. Enabling queues a webhook_test and returns its test_event_id without waiting for successful delivery.

See the Webhooks API for replacement updates, secret retention, test requests, and event lookup. The MCP webhook tools support the same operations. MCP connections cannot subscribe to API key events because they cannot grant api_keys:read.

Every POST has Content-Type: application/json and the same outer object, or envelope. The envelope identifies the event. Its data field contains the payload for that event’s type.

{
"event_id": "example_event_id",
"type": "image_stored",
"schema_version": 1,
"occurred_at": "2026-10-29T14:00:00Z",
"source": {
"channel": "api",
"api_id": "your-api-id",
"ip": "203.0.113.10",
"user_agent": "my-image-service/1.0"
},
"data": {
"image_id": "01a135fb-21fa-7027-9b82-70acc0e1fd16",
"width": 1200,
"height": 630,
"file_size_bytes": 48210,
"location": "hcti_storage",
"location_details": null
}
}
Field Meaning
event_id Unique ID of the logical event. The same event keeps this ID across retries and all webhook endpoints receiving it. Treat it as an opaque string.
type Event name, such as image_stored or template_updated. Determines the shape of data.
schema_version Version of this event type’s data shape. Increases for breaking changes. Accept additional fields without failing.
occurred_at UTC time the event happened, rather than the time this delivery attempt was sent.
source Who or what caused the event. Its channel identifies the variant below.
data Event-specific details. The event reference below describes each shape.
Source attribution
source.channel Additional fields
api api_id identifies the authenticating key. Includes ip and user_agent when known.
dashboard user_id identifies the signed-in user. Includes ip and user_agent when known.
mcp client_name, tool_name, ip, and user_agent when known.
anonymous ip and user_agent when known, such as for a public image URL request.
system HCTI background work, billing updates, or support actions. No caller details.

Attribution describes the call that caused the event. For a lazy-rendered image, this may be the request that fetched the image, rather than the earlier API request that created its URL. Nullable attribution fields can be absent or null.

Templates, proxies, storage destinations, Open Graph configurations, and API keys share three event patterns. Each uses the envelope above, with an entity object inside data identifying the resource by its type and id.

Event pattern Structure of data Meaning
*_created { "entity": { "type": "...", "id": "..." }, "properties": { ... } } A snapshot of the resource’s properties when it was created.
*_updated { "entity": { "type": "...", "id": "..." }, "changes": { ... } } Only the properties that changed, keyed by property path.
*_deleted { "entity": { "type": "...", "id": "..." } } Identifies the deleted resource without copying its contents.

For example, template_created and proxy_created have the same outer structure but different properties. Update events use dotted paths for nested properties. A request that leaves all public properties unchanged does not produce an update event.

Each entry in changes has a kind that describes how to read it:

Change kind Fields and interpretation
value previous_value and new_value. Scalars and arrays are reported as whole values. null means unset or cleared. Long text values can be truncated as described below.
secret previous_assigned and new_assigned. Reports whether a credential was set, without exposing its value.
content No value fields. Reports a change to content such as a block template’s design. Read the resource for its current contents.

Image, organization, and test events have their own data shapes. For example, image_deleted contains image_ids rather than an entity object. See the event definitions for each payload.

Stored passwords, API key secrets, storage secret access keys, and webhook signing secrets are never included in event payloads. Public identifiers such as api_id and a storage access_key_id can be included to identify the key or connection.

Creation events report credential state through flags such as password_assigned and secret_access_key_assigned. Update events use kind: "secret" with previous_assigned and new_assigned. Replacing an existing password reports true for both flags without including either password.

Large text values such as html and css may be truncated to keep payloads small. These fields use a text object rather than a plain string, including when they appear as previous_value or new_value in an update:

Field Meaning
value The full text when truncated is false, or the beginning when it is true.
truncated Whether the text was shortened.
length The original text’s length in characters.
ender The original text’s final characters when truncated, otherwise null.

When truncated is true, fetch the resource if you need its full current content. A block template’s internal design is reported as a content change without its contents.

Header Meaning
webhook-id Stable ID for this event’s delivery to this webhook endpoint. Retries reuse it. This is not the webhook configuration’s ID or the envelope’s event_id.
webhook-timestamp Unix timestamp in seconds for this send attempt.
webhook-signature Present when signing is enabled. Uses the Standard Webhooks v1,<base64 signature> format.
x-hcti-event-id The envelope’s event_id.
x-hcti-attempt Attempt number, starting at 1.
user-agent HCTI-Webhooks/1.0.

Use webhook-id to deduplicate a delivery at one receiver. If multiple endpoints feed the same workflow, you can use the envelope’s event_id to deduplicate the event across those endpoints. Store the deduplication record atomically with accepting or processing the event so simultaneous deliveries cannot both start the same work.

Turn on Sign deliveries and use the webhook’s signing secret to verify the raw request body before parsing it. Standard Webhooks verifier libraries can check these headers. Re-serializing JSON can change whitespace or property order and invalidate the signature.

The signature is an HMAC-SHA256 of:

webhook-id.webhook-timestamp.raw-request-body

Strip the whsec_ prefix from the signing secret and base64-decode the remainder to obtain the HMAC key. Compare signatures in constant time and reject stale timestamps to limit replay. A retry has a fresh timestamp and signature but retains its delivery ID.

The runnable Node.js receiver below verifies the original request bytes and accepts timestamps within five minutes of the server’s clock. Keep the secret in server-side configuration. Use the verified envelope’s event_id and data when deciding what work to perform.

HCTI uses at-least-once delivery: your receiver must be able to accept the same event more than once. Event recording and HTTP delivery are separate, so an unavailable receiver does not roll back the change.

Event family How the event is recorded
Template, proxy, storage destination, Open Graph configuration, and API key lifecycle events Recorded in the same database transaction as the resource change.
image_stored and image_deleted Recorded through a durable outbox alongside the image’s storage metadata or deletion markers, then relayed for delivery. This is separate from the file upload itself.
organization_plan_changed Recorded with the effective plan change.
organization_usage_alert Recorded with the threshold alert record, independently of whether the notification email succeeds.
organization_usage_reset Recorded with the billing period update after resetting usage. The usage counter reset is a separate operation.
webhook_test A synthetic delivery check, rather than a resource change.

These recording rules apply to matching subscriptions. They do not replay changes from before a webhook was enabled.

At-least-once does not mean unlimited retries or guaranteed success at an unreachable endpoint. Delivery can end in failure after its retry window, or be invalidated when a webhook is disabled, deleted, or unsubscribed. An HTTP request may reach your application even if its response is lost, causing another attempt.

Endpoint result Delivery behavior
Any 2xx Success. No automatic retry.
Timeout, connection/DNS/TLS failure, or transient internal delivery error Automatic retry.
408, 429, or 5xx Automatic retry.
Other 4xx, a redirect, or a blocked destination Final failure. Redirects are not followed.

A delivery gets up to 5 automatic attempts in total: the initial attempt, then retries after approximately 5 minutes, 1 hour, 12 hours, and 2 days, each measured from the previous failed attempt. Retry-After can delay the next attempt further but does not add attempts. No attempt is scheduled beyond 3 days after the event occurred. An endpoint has about 10 seconds to respond.

There is currently no manual retry or replay action in the dashboard, REST API, or MCP. Once a delivery reaches final failure or its retry window ends, fixing your endpoint does not resend that event. Sending a test checks the endpoint with a new webhook_test event. It does not replay the failed business event.

Use delivery history to identify failed events and inspect their payloads. Reconcile your application’s state with the current image or resource, then enqueue any missing work in your own system. If you recover a historical payload, use its original event_id in the same deduplication path as normal deliveries. Keep a durable inbox of received events so you can retry your own processing independently of HCTI’s delivery window.

  1. Verify the signature and parse the envelope.
  2. Durably accept the event, using a unique delivery or event ID to prevent duplicate processing. Acknowledge known duplicates with 2xx too.
  3. Return 2xx promptly, then perform longer work from your own queue. If you cannot durably accept the event, return a retryable error such as 503.
  4. Make downstream actions idempotent as well, so restarting a worker does not publish the same post or send the same email twice.

Events can arrive concurrently and out of order. Retries of older events can arrive after newer changes. Use event timestamps and resource versions where available. Fetch the current resource if your workflow needs its latest state. An update payload describes that change, not necessarily the state at the time you receive it.

This example uses Node.js 24 and its built-in SQLite module. It verifies signed POST requests at /hcti, commits each event to a durable inbox, and returns 204 for accepted events and known duplicates. It also accepts webhook_test, marking it processed without starting business work. A failed database write returns 503 so HCTI can retry.

Download webhook-receiver.mjs, set HCTI_WEBHOOK_SECRET to the secret revealed in the dashboard, and run:

Terminal window
node webhook-receiver.mjs

The receiver listens at http://127.0.0.1:3000/hcti. Put it behind your public HTTPS endpoint or a development HTTPS tunnel, and preserve the raw request body. PORT changes the local port. WEBHOOK_DB changes the database path, which defaults to ./webhooks.sqlite.

Complete receiver source
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
import { DatabaseSync } from 'node:sqlite';
const secret = process.env.HCTI_WEBHOOK_SECRET;
if (!secret?.startsWith('whsec_')) {
throw new Error('Set HCTI_WEBHOOK_SECRET to your dashboard signing secret');
}
// Keep this file and its SQLite sidecar files on persistent local storage.
const db = new DatabaseSync(process.env.WEBHOOK_DB || './webhooks.sqlite');
db.exec(`
PRAGMA journal_mode = WAL;
PRAGMA synchronous = FULL;
PRAGMA busy_timeout = 1000;
CREATE TABLE IF NOT EXISTS webhook_inbox (
event_id TEXT PRIMARY KEY,
type TEXT NOT NULL,
payload TEXT NOT NULL,
received_at TEXT NOT NULL,
processed_at TEXT
) STRICT;
`);
const accept = db.prepare(`
INSERT INTO webhook_inbox (event_id, type, payload, received_at, processed_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (event_id) DO NOTHING
`);
function verifyWebhook(rawBody, headers) {
const id = headers.get('webhook-id');
const timestamp = headers.get('webhook-timestamp');
const signatures = headers.get('webhook-signature');
if (!id || !timestamp || !signatures || !/^\d+$/.test(timestamp)) {
throw new Error('Missing or invalid webhook headers');
}
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) {
throw new Error('Webhook timestamp outside tolerance');
}
const key = Buffer.from(secret.slice(6), 'base64');
const expected = createHmac('sha256', key)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest();
const valid = signatures.split(/\s+/).some(signature => {
const [version, encoded] = signature.split(',');
if (version !== 'v1' || !encoded) return false;
const received = Buffer.from(encoded, 'base64');
return received.length === expected.length &&
timingSafeEqual(received, expected);
});
if (!valid) throw new Error('Invalid webhook signature');
}
const server = createServer(async (request, response) => {
if (request.method !== 'POST' || request.url?.split('?')[0] !== '/hcti') {
response.writeHead(404).end();
return;
}
let rawBody;
try {
const chunks = [];
let bytes = 0;
for await (const chunk of request) {
bytes += chunk.length;
if (bytes > 8 * 1024 * 1024) {
response.writeHead(413).end();
return;
}
chunks.push(chunk);
}
rawBody = Buffer.concat(chunks);
verifyWebhook(rawBody, new Headers(request.headers));
} catch {
response.writeHead(400).end('Invalid webhook request');
return;
}
let event;
try {
event = JSON.parse(rawBody.toString('utf8'));
if (typeof event?.event_id !== 'string' || !event.event_id ||
typeof event.type !== 'string' || !event.type) {
throw new Error('Invalid envelope');
}
} catch {
response.writeHead(400).end('Invalid event envelope');
return;
}
try {
const now = new Date().toISOString();
// This single committed insert both deduplicates and queues business events.
// Test events are recorded as processed and never start business work.
accept.run(event.event_id, event.type, rawBody.toString('utf8'), now,
event.type === 'webhook_test' ? now : null);
response.writeHead(204).end();
} catch (error) {
console.error('Could not persist webhook:', error.code);
response.writeHead(503).end('Try again later');
}
});
server.listen(Number(process.env.PORT || 3000), '127.0.0.1', () => {
console.log(`Webhook receiver: http://127.0.0.1:${server.address().port}/hcti`);
});

The inbox is also the queue. A separate worker can select rows where processed_at IS NULL, perform the action using event_id as its idempotency key, and set processed_at only after success. Handle unfamiliar event types or schema versions deliberately rather than assuming they have an existing payload shape. A worker crash can repeat an action before marking the row, so downstream idempotency still matters.

This example deduplicates by event_id for one organization’s workflow, including deliveries from multiple webhook endpoints. Preserve the database and its sidecar files on persistent local storage across restarts. For receivers running across multiple machines, use a shared durable database or queue with equivalent atomic deduplication. Keep deduplication records for as long as events can be retried or recovered in your workflow.

Image creation usually returns an image ID and URL before rendering. Rendering starts when the image URL is requested, or when your application calls the authenticated store URL for custom-storage-only output. Registering a webhook does not start a render.

image_stored reports a saved base file, with one event per location: hcti_storage or customer_storage. Saving to both locations produces two events. Cached responses, resized or reformatted copies of an already stored image, and debugger captures do not produce another base-storage event.

Do not assume exactly one event per image ID. Separate concurrent renders can produce distinct storage events, each with its own event_id. These are different from retries of one event. If your business action should happen once per image and storage location, enforce that rule using a key such as (image_id, location) in addition to delivery deduplication. For bucket workflows, also inspect location_details to identify the destination and object.

Open Webhook History, or choose View delivery history on a webhook to filter by that destination. Open an event to see its envelope and payload, destination statuses, attempt times, HTTP results, and the configuration version used for each attempt. Response bodies and response headers are not retained.

History is retained for 365 days. Very large payloads may be unavailable in older archived history. A missing payload does not mean an event was never delivered. Secret delivery parameter values are never shown.

GET /v1/webhooks/events/{event_id} and MCP get_webhook_event retrieve one known event. Reading its payload also requires read access to its resource. Without that access, data is null and payload_withheld is true. The dashboard provides the browsable history list.

Pending and retry attempts use the webhook’s current delivery settings when dispatched. Disabling or deleting it, or removing its event subscription, stops queued deliveries. A request already in flight may still reach your endpoint. Creating, enabling, or changing an enabled webhook’s URL, parameters, or signing setting queues a webhook_test. Renaming it or changing only its subscriptions does not queue a test.

Expand a resource group, then an event for its definition and example. The JSON examples in this section show data only. Every delivery wraps this data in the event envelope. Examples use illustrative IDs and values. Nullable fields and additional rendering/provider settings may also be present.

Resource lifecycle events follow the shared entity structure, including its rules for secret fields and truncated text.

Images

Subscription and payload access require images:read.

image_stored — a rendered base file was saved

image_id identifies the saved image. width and height are the stored base image’s pixel dimensions before requested transformations. They can be null for PDFs or older events. file_size_bytes is the base file size and can be null for older events.

location is hcti_storage or customer_storage. For customer storage, location_details contains storage_destination_id, bucket, and path (the object key including its prefix). Otherwise it is null.

{
"image_id": "01a135fb-21fa-7027-9b82-70acc0e1fd16",
"width": 1200,
"height": 630,
"file_size_bytes": 48210,
"location": "customer_storage",
"location_details": {
"storage_destination_id": "example_storage_id",
"bucket": "social-images",
"path": "posts/01a135fb-21fa-7027-9b82-70acc0e1fd16.png"
}
}

See image events and concurrent renders before relying on one notification per image.

image_deleted — a delete request removed images

One event per delete request. image_ids lists the images actually deleted: one ID for a single delete, or multiple IDs for a batch. Already-deleted images are excluded. The event name is singular even when the array contains multiple images.

{
"image_ids": [
"01a135fb-21fa-7027-9b82-70acc0e1fd16",
"01a13603-b907-7f63-a402-56d9c89865bd"
]
}
Templates

Subscription and payload access require templates:read.

template_created — a template was created

entity.type is template. properties includes its version, name, description, and stored rendering overrides, such as viewport_width, device_scale, proxy_id, or storage_destination_id. Nullable overrides mean the setting is unset. The template_type discriminator identifies the additional fields:

  • html_css: html and css are nullable text objects with truncation metadata. google_fonts lists families separated by |.
  • blocks: block_counts maps block type names to counts. variables lists discovered variables and their value requirements.

Example HTML/CSS properties:

{
"entity": { "type": "template", "id": "example_template_id" },
"properties": {
"template_type": "html_css",
"version": 12345,
"name": "Social card",
"description": null,
"html": { "value": "<h1>Hi</h1>", "truncated": false, "length": 11, "ender": null },
"css": null,
"google_fonts": null,
"viewport_width": 1200,
"viewport_height": 630
}
}

The stored overrides use the template rendering parameters, rather than resolved defaults. A block template’s internal design is not included.

template_updated — a template changed

entity.type is template. changes can include metadata, rendering options, version, html, css, google_fonts, block_counts, or variables. HTML/CSS changes carry truncated text objects as values. A block design change uses content: { kind: "content" }.

{
"entity": { "type": "template", "id": "example_template_id" },
"changes": {
"name": { "kind": "value", "previous_value": "Social card", "new_value": "Product card" },
"version": { "kind": "value", "previous_value": 12345, "new_value": 12346 }
}
}
template_deleted — a template was deleted

Identifies the deleted template. It does not include a copy of its contents.

{ "entity": { "type": "template", "id": "example_template_id" } }
Proxies

Subscription and payload access require proxies:read.

proxy_created — a proxy was created

properties contains name, url, nullable port, bypass_hosts, nullable username, password_assigned, and enabled. Passwords are never included. A null port uses the proxy URL scheme’s default.

{
"entity": { "type": "proxy", "id": "example_proxy_id" },
"properties": {
"name": "Rendering proxy",
"url": "https://proxy.example.com",
"port": 8443,
"bypass_hosts": [],
"username": "render-user",
"password_assigned": true,
"enabled": true
}
}
proxy_updated — a proxy changed

entity.type is proxy. Ordinary changes use kind: "value". The username and password change paths are top-level fields. Password changes report assignment state without the password.

{
"entity": { "type": "proxy", "id": "example_proxy_id" },
"changes": {
"enabled": { "kind": "value", "previous_value": true, "new_value": false },
"password": { "kind": "secret", "previous_assigned": true, "new_assigned": true }
}
}
proxy_deleted — a proxy was deleted

Identifies the deleted proxy configuration.

{ "entity": { "type": "proxy", "id": "example_proxy_id" } }
Storage destinations

Subscription and payload access require storage_destinations:read.

storage_destination_created — a storage destination was created

properties contains name, enabled, hcti_storage_disabled, provider-specific connection_info, and secret_access_key_assigned. Connection details use the storage destination response shape, without secret access keys. Amazon S3 uses a role and reports secret_access_key_assigned: false.

{
"entity": { "type": "storage_destination", "id": "example_storage_id" },
"properties": {
"name": "Social images",
"enabled": true,
"hcti_storage_disabled": false,
"connection_info": {
"provider": "cloudflare_r2",
"cloudflare_account_id": "0123456789abcdef0123456789abcdef",
"access_key_id": "your-access-key-id",
"bucket": "social-images",
"key_prefix": "posts/"
},
"secret_access_key_assigned": true
}
}
storage_destination_updated — a storage destination changed

entity.type is storage_destination. changes includes ordinary configuration changes and secret assignment changes. Provider fields use dotted paths under connection_info. The secret assignment uses the top-level path secret_access_key.

{
"entity": { "type": "storage_destination", "id": "example_storage_id" },
"changes": {
"connection_info.bucket": { "kind": "value", "previous_value": "old-images", "new_value": "social-images" },
"secret_access_key": { "kind": "secret", "previous_assigned": true, "new_assigned": true }
}
}
storage_destination_deleted — a storage destination was deleted

Identifies the deleted destination configuration. Deleting a destination does not delete files in your bucket.

{ "entity": { "type": "storage_destination", "id": "example_storage_id" } }
Open Graph configurations

Subscription and payload access require og_configs:read.

og_config_created — an Open Graph configuration was created

properties contains config_type, name, description, base_url, optimization_mode, enabled, and refresh_interval_s, plus fields for its variant:

  • html_css: default_options, a separate truncated css field, and extract_values. CSS from the defaults is reported in css.
  • templated: template_id, nullable template_version, template_values_mapping, headers, and additional_header_origins.

Example screenshot configuration:

{
"entity": { "type": "og_config", "id": "example_og_config_id" },
"properties": {
"config_type": "html_css",
"name": "Website cards",
"description": null,
"base_url": "https://example.com",
"optimization_mode": "no_optimization",
"enabled": true,
"refresh_interval_s": 86400,
"default_options": { "viewport_width": 1200, "viewport_height": 630 },
"css": null,
"extract_values": false
}
}

See OG configuration fields for the variant-specific options.

og_config_updated — an Open Graph configuration changed

entity.type is og_config. Changes include common and variant-specific fields. Nested defaults use dotted paths. Arrays are reported as whole values. CSS uses truncated text values.

{
"entity": { "type": "og_config", "id": "example_og_config_id" },
"changes": {
"refresh_interval_s": { "kind": "value", "previous_value": 86400, "new_value": 3600 }
}
}
og_config_deleted — an Open Graph configuration was deleted

Identifies the deleted configuration.

{ "entity": { "type": "og_config", "id": "example_og_config_id" } }
API keys

Subscription and payload access require api_keys:read. These subscriptions are available through the dashboard or REST, rather than MCP. Key secrets are never included.

api_key_created — an API key was created

properties contains api_id, nullable name and description, enabled, all_future_permissions, and a permissions array. Use entity.id for management requests and api_id as the authentication username.

{
"entity": { "type": "api_key", "id": "example_key_id" },
"properties": {
"api_id": "your-api-id",
"name": "Publishing service",
"description": null,
"enabled": true,
"all_future_permissions": false,
"permissions": ["images:create", "images:read"]
}
}
api_key_updated — an API key’s metadata or permissions changed

entity.type is api_key. Changes report metadata, enabled state, all-future access, or permissions. Permission arrays are reported as whole values.

{
"entity": { "type": "api_key", "id": "example_key_id" },
"changes": {
"permissions": {
"kind": "value",
"previous_value": ["images:create"],
"new_value": ["images:create", "images:read"]
}
}
}
api_key_deleted — an API key was deleted

Identifies the deleted key by its management ID.

{ "entity": { "type": "api_key", "id": "example_key_id" } }
Organization

Subscription and payload access require usage:read.

organization_plan_changed — the effective plan changed

previous_plan and new_plan are public tier names: Free, Basic, Pro, Scale, or No Plan. Their corresponding *_credits_allowed fields give included image credits per billing period. These values can be null when unknown. changed_at is the UTC time the change took effect.

{
"previous_plan": "Basic",
"previous_credits_allowed": 50000,
"new_plan": "Pro",
"new_credits_allowed": 100000,
"changed_at": "2026-10-29T14:00:00Z"
}

Credit values above are illustrative. Use the delivered limits for the organization’s actual plan.

organization_usage_reset — a new usage period started

previous_period_credits_used is the previous period’s usage, or null if unavailable. period_start is the new period’s UTC start. period_end is its end or null if unknown. plan and credits_allowed describe the new period’s plan and can be null.

{
"previous_period_credits_used": 42310,
"period_start": "2026-10-29T00:00:00Z",
"period_end": "2026-11-29T00:00:00Z",
"plan": "Basic",
"credits_allowed": 50000
}
organization_usage_alert — usage crossed a threshold

One logical event is raised per threshold per usage period. Retries can deliver it more than once. threshold_percent is 80 or 100. credits_used is usage when the alert was raised. credits_allowed is the plan’s included allowance. period_start identifies the billing period. Concurrent rendering can make the reported usage exceed the exact threshold.

{
"threshold_percent": 80,
"credits_used": 40002,
"credits_allowed": 50000,
"period_start": "2026-10-29T00:00:00Z"
}

The event does not itself change overage settings or stop rendering.

Test deliveries
webhook_test — check endpoint delivery

A synthetic event sent only to the webhook being tested. You cannot subscribe to it. It is sent automatically on relevant configuration changes or through the test action, with the webhook’s normal parameters and signing setting.

webhook_id is the configuration ID being tested. config_version is the version of its delivery settings when the test was queued. Inspect the attempt’s configuration in history to see the settings actually used if the webhook changed before dispatch.

{
"webhook_id": "example_webhook_id",
"config_version": 1
}

Reading a test payload needs only webhooks:read.

Need help?

Talk to a human. Email support@htmlcsstoimage.com and we’ll help you get started.