Related: Concepts · Publish Flow · Open API
Webhooks deliver HTTP POST notifications to external URLs when content or translation events occur in a space. Each webhook is scoped to a space, can subscribe to multiple events, and optionally signs payloads with HMAC-SHA256.
spaces/{spaceId}/webhooks/{webhookId} — webhook config
spaces/{spaceId}/webhooks/{webhookId}/logs/{logId} — execution history (not capped or pruned; one doc per dispatch)
Access control: SPACE_MANAGEMENT (or admin) required to read/write webhook configs and to read logs. Logs are client read-only; they are written by Functions via the Admin SDK.
interface WebHook {
name: string;
url: string;
enabled: boolean;
events: WebHookEvent[];
headers?: Record<string, string>; // custom request headers
secret?: string; // HMAC-SHA256 signing key
createdAt: Timestamp;
updatedAt: Timestamp;
}| Event | Value | Triggered by |
|---|---|---|
CONTENT_PUBLISHED |
content.published |
publish callable |
CONTENT_UNPUBLISHED |
content.unpublished |
unpublish callable |
CONTENT_CHANGED |
content.changed |
onContentUpdate / onContentDelete (Firestore onDocumentUpdated/onDocumentDeleted triggers) |
TRANSLATION_PUBLISHED |
translation.published |
publish callable |
TRANSLATION_CHANGED |
translation.changed |
generateTranslationsDraft() |
interface WebHookPayload {
event: WebHookEvent;
spaceId: string;
timestamp: string; // ISO 8601
data: ContentWebHookPayloadData | TranslationWebHookPayloadData;
signature?: string; // internal only — the signature is delivered in the X-Webhook-Signature header, not the body
}Content events send data: { id, fullSlug } (ContentWebHookPayloadData) — the content's ID and full slug. Translation events send an empty data object.
Every webhook dispatch sends:
POST <webhook.url>
Content-Type: application/json
User-Agent: Localess-WebHook/1.0
X-Webhook-Event: <event>
X-Webhook-Delivery: <uuid> ← unique per dispatch; same value as the log's deliveryId
X-Webhook-Signature: sha256=<hex> ← only if secret is set
<custom headers from webhook.headers, minus reserved ones — see below>
Body: JSON-serialised WebHookPayload (without `signature`)
- Timeout: 30 seconds
- Retries: none
- Redirects: not followed. A 3xx is logged as an
httpfailure with its status code. - Response body: at most 4 KB is read, which is all that gets logged.
- All enabled webhooks for an event fire concurrently via
Promise.allSettled()(one failure does not block others). Each webhook signs its own copy of the payload, so no webhook ever sees another's signature.
Delivery runs inside the project with the Functions service account, and the URL and headers are chosen by a space manager. So functions/src/utils/webhook-request.ts limits where a webhook can go. Without these limits, a webhook, or a redirect from one, could reach the GCP metadata server (169.254.169.254, which issues the service account's OAuth token) and the reply would show up in the webhook log.
- URL (
checkWebhookUrl):https:only, on the default port, with no credentials in the URL.localhost,*.localhost,*.internal,metadata.google.internaland private IP literals are refused. The WHATWG URL parser normalises shorthand forms such as0x7f.1and2130706433before the check. - Resolved address (
isBlockedAddress, applied in the socket's DNSlookup): the connection is refused if the host resolves to any loopback, private, link-local, CGNAT, multicast, reserved or documentation range, IPv4 or IPv6, including IPv4-mapped IPv6. The check runs on the exact address being connected to, so DNS rebinding can't get around it. - Headers (
sanitizeWebhookHeaders): custom headers can't setHost,Metadata-Flavor,Content-Length,Transfer-Encoding,Connection,Content-Type,User-Agent, or anyX-Webhook-*header. Those are dropped. - Emulator (
FUNCTIONS_EMULATOR=true):http:and local or private targets are allowed, so a local receiver can be used.
A refused delivery sends nothing. It is logged as a network failure with errorMessage "Webhook URL is not allowed: …".
When a webhook has a secret, the payload is signed before dispatch:
body = JSON.stringify(payload) // serialised once, before signing
signature = HMAC-SHA256(secret, body)
header: X-Webhook-Signature: sha256=<hex_digest>
The signature is delivered only in the X-Webhook-Signature header. Receivers must read it from the header and verify it by computing the HMAC over the raw request body exactly as received (do not re-serialise parsed JSON, and do not rely on any signature field in the body).
Every dispatch — success or failure — writes a log entry to the logs subcollection. WebHookLog is a discriminated union on status:
enum WebHookStatus {
SUCCESS = 'success',
FAILURE = 'failure',
}
enum WebHookErrorType {
TIMEOUT = 'timeout',
NETWORK = 'network',
HTTP = 'http',
}
interface WebHookLogBase {
event: WebHookEvent;
url: string;
requestSize: number;
data: ContentWebHookPayloadData | TranslationWebHookPayloadData;
deliveryId: string;
duration: number; // milliseconds
createdAt: Timestamp;
}
interface WebHookLogSuccess extends WebHookLogBase {
status: WebHookStatus.SUCCESS;
statusCode: number;
statusText: string;
responseBody?: string;
responseBodyTruncated?: boolean;
}
interface WebHookLogFailure extends WebHookLogBase {
status: WebHookStatus.FAILURE;
errorType: WebHookErrorType; // timeout | network | http
statusCode?: number;
statusText?: string;
responseBody?: string;
responseBodyTruncated?: boolean;
errorMessage?: string;
}
type WebHookLog = WebHookLogSuccess | WebHookLogFailure;Logs are not capped on write — every dispatch adds a document and nothing prunes them (they are only removed with the webhook, see Cleanup). Limits apply only on read: the backend findWebHookLogs() query returns the latest 100 by createdAt descending, and the frontend WebHookService.findLogs() takes an optional max. The WebhookDetailComponent shows the log history with pagination and filtering.
| File | Event |
|---|---|
functions/src/contents.ts — publish callable |
CONTENT_PUBLISHED |
functions/src/contents.ts — unpublish callable |
CONTENT_UNPUBLISHED |
functions/src/contents.ts — onContentUpdate / onContentDelete |
CONTENT_CHANGED |
functions/src/translations.ts — publish callable |
TRANSLATION_PUBLISHED |
functions/src/translations.ts / functions/src/services/translation.service.ts — generateTranslationsDraft() |
TRANSLATION_CHANGED |
All call triggerWebHooksForEvent(spaceId, payload) from functions/src/utils/webhook-utils.ts.
A Firestore onDocumentDeleted trigger in functions/src/webhooks.ts recursively deletes the logs subcollection when a webhook document is deleted.
Routes: part of the Developers module (see Open API for the sibling module and route shell)
/features/spaces/:spaceId/developers/webhooks [DEV_WEBHOOK] ← WebhooksComponent (list)
/features/spaces/:spaceId/developers/webhooks/:webhookId [DEV_WEBHOOK] ← WebhookDetailComponent
Form validation (WebhookDialogComponent):
name: required, 3–50 chars, no leading/trailing spacesurl: required, max 2048 chars,https://(orhttp://localhost/http://127.0.0.1for local development) — error keywebhookUrl.firestore.rulesenforces the same shape on create and update. Delivery re-checks it and blocks private addresses, see Destination restrictions.events: required, at least one selectedsecret: optional, displayed as a password field
Webhooks are created with enabled: true by default. WebhooksComponent (list view) supports enable/disable toggle, edit, delete, and navigating into a webhook's detail page.
WebhookDetailComponent shows the full log history for a single webhook: search by log id, filter by event/status, paginated (HlmPaginationImports), plus enable/disable, edit, and delete actions.
| File | Purpose |
|---|---|
functions/src/models/webhook.model.ts |
Backend types — WebHook, WebHookEvent, WebHookPayload, WebHookLog |
functions/src/services/webhook.service.ts |
Backend Firestore queries |
functions/src/utils/webhook-utils.ts |
HTTP dispatch, HMAC signing, execution logging |
functions/src/utils/webhook-request.ts |
Destination checks (URL, resolved address, headers), guarded POST |
functions/src/webhooks.ts |
onDocumentDeleted cleanup trigger |
src/app/shared/models/webhook.model.ts |
Frontend types |
src/app/shared/services/webhook.service.ts |
Frontend Firestore CRUD + log queries |
src/app/shared/validators/webhook.validator.ts |
Form validators |
src/app/features/spaces/developers/webhooks/ |
UI — list (webhooks.component), create/edit (webhook-dialog/), detail + log history (webhook-detail/) |