Outbound webhooks
An outbound webhook makes Loha send a signed POST to a URL you control whenever
a build finishes. It's how you close the loop programmatically: start a build, then be
told when it's done and with what status. No polling.
These are the opposite direction from the webhook URL on a trigger. A trigger webhook is an endpoint Loha exposes so your git provider can start a build (inbound). An outbound webhook is an endpoint you expose so Loha can notify you when a build completes (outbound). This page is about the latter.
What you can do with them
- Notify a chat or incident tool: relay build results to Slack, Teams, or an on-call system when a deploy finishes.
- Alert on failures only: subscribe to
FAILED,TIMEOUT, andCANCELLEDand page someone when a pipeline breaks. - Chain downstream automation: kick off a smoke test, a cache warm, or a dependent service deploy when a specific pipeline succeeds.
- Close the trigger-then-done loop: fire a build with
POST /api/v1/triggers/{id}/runand let the webhook tell you the outcome, instead of pollingGET /api/v1/builds/{id}.
Events
Loha emits six event types:
| Event type | Fires when |
|---|---|
com.loha.build.completed.v1 | A build reaches a terminal status: SUCCESS, FAILED, TIMEOUT, CANCELLED. |
com.loha.release.produced.v1 | A successful build pushed a container image. One event per run, listing every image and its digest. |
com.loha.usage.period.closed.v1 | A calendar month closed for your workspace and its usage snapshot was written. |
com.loha.security.issue.new_critical.v1 | A scan report found CRITICAL security issues that are new, reopened or newly raised to CRITICAL. One event per report. |
com.loha.security.issue.sla_breached.v1 | Security issues passed their fix deadline. One event per check, for the workspace. |
com.loha.security.issue.secret_detected.v1 | A push to a tracked branch found new or reopened secrets (secret scanning on push), at any severity. One event per push. |
New webhooks listen to build completion only unless you tick another event. Every addition is opt-in so an existing receiver never starts getting a second, differently shaped payload it wasn't written for.
Send ping delivers a com.loha.webhook.ping.v1 to your endpoint regardless of which
events the subscription lists — that is the point of a test, and it is the one type not
in the table above. If your receiver validates the incoming type against an allowlist
(a good habit, and one the signature advice encourages), add
com.loha.webhook.ping.v1 to it or your endpoint will reject its own test delivery and
report the wiring broken when it is fine.
Releases
com.loha.release.produced.v1 is the "an image shipped, go deploy it" hook. Its payload
carries sourceRunId, workspaceId, repositoryId, commitSha and an images array
of { imageUrl, imageDigest }: the bare repository ref and the full sha256: manifest
digest. Join them with @ for a deployable, immutable reference:
harbor.example.com/acme/api@sha256:…. A build that pushes two images produces one
event listing both.
It is a fact about images, not about a run, which is why it deliberately carries no build status and no trigger id (see the scoping note below). For what a release records and where to browse them, see Releases.
Usage periods
com.loha.usage.period.closed.v1 fires once per workspace when a calendar month is
closed and its immutable usage snapshot is written — the hook to reconcile a billing
period against your own systems. Its data carries workspaceId, period
(YYYY-MM), periodStart and periodEnd (ISO 8601), buildMinutes, buildCount
and planKey.
It fires once per period per workspace, on the snapshot being newly written, so a re-run of the close for an already-closed period sends nothing. Like a release it is a fact about a workspace, not about a run, so it carries no status and no trigger id.
Security issues
Three events report on security issues, and only for workspaces that have security issues enabled. The Webhooks page offers them only while security issues are enabled for the workspace.
com.loha.security.issue.new_critical.v1fires when a scan report finds CRITICAL issues that are new, reopened, or newly raised to CRITICAL. One event per report, however many issues it covers.com.loha.security.issue.sla_breached.v1fires when issues pass their fix deadline. One event per check, covering the issues that newly passed it. An issue is reported once per deadline: reopening it, revoking its triage or its accepted risk expiring starts a new deadline, which can be reported again.com.loha.security.issue.secret_detected.v1fires when a push to the default branch or a protected ref finds secrets that are new or reopened, at any severity. One event per push. Its secrets never also appear innew_critical.
Their data carries workspaceId, workspaceSlug (can be null), total (how many
issues the event covers) and issues, which lists at most 20 of them (the most
severe and longest overdue first, for a breach). new_critical and secret_detected also carry
assessmentId, buildId and repositoryId (the last two can be null; the buildId of secret_detected is always null). Each entry in
issues has id, severity, category, ruleId, packageName, title, slaDueAt
and ownerUserId; the last five can be null, and packageName is always null for a
secret.
The payload never carries a file path, version, line number, fingerprint or matched text, so it does not carry a secret's value. Like a release, these events are facts about a workspace, not about a run, so they carry no status and no trigger id.
Scope a subscription
A build-completion subscription is scoped along two axes so you only receive what you care about:
- Source: whole workspace (every build) or a single trigger (only builds fired by that trigger).
- Status: all terminal statuses, or a subset (e.g. failures only).
com.loha.release.produced.v1, com.loha.usage.period.closed.v1 and the three
com.loha.security.issue.* events carry no status and no trigger id, so a subscription
with a status filter or a specific trigger set will never receive any of them.
Keep those subscriptions whole-workspace with all statuses, or put them on their own
webhook.
Notification channels (Microsoft Teams) can render build completion and the security issue events, and nothing else, so they cannot subscribe to release or usage events. The API rejects the combination rather than accepting a channel that would silently post nothing. To post security issue alerts to Teams, create a channel subscribed to the security issue events and leave its scope on the whole workspace with all statuses.
Set one up
In the dashboard
- Go to Settings → Webhooks and click Create webhook (you need the workspace admin or owner role).
- Fill in:
- Endpoint URL: must be public HTTPS (see Security).
- Events: build completion (ticked by default), release produced, and, when security issues are enabled, the security issue events. At least one must stay ticked.
- Scope: Whole workspace or A specific trigger.
- Build statuses: All statuses, failures, or successes.
- Description: optional label.
- Click Create. The signing secret is shown once. Copy it now and store it where your receiver can read it. You can't see it again; you can only rotate it.
You can also create a webhook pre-scoped to a trigger from the Outbound webhooks section on that trigger's detail page.
Via the API
Create a webhook with a PAT or service-account key
carrying the webhooks:write scope:
curl -X POST https://app.loha.longtumlab.com/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/loha",
"statusFilter": ["FAILED", "TIMEOUT", "CANCELLED"]
}'
The response includes the secret once:
{
"webhook": { "id": "wh_…", "url": "https://example.com/hooks/loha", "enabled": true },
"secret": "whsec_…"
}
To scope to a single trigger, either pass "triggerId": "trg_…" or post to
POST /api/v1/triggers/{id}/webhooks (the path trigger always wins). See the full
Webhooks API reference.
What Loha sends
Each delivery is an HTTP POST with a CloudEvents 1.0
structured-JSON body:
POST /hooks/loha HTTP/1.1
Content-Type: application/cloudevents+json; charset=utf-8
x-loha-signature: sha256=<hex>
x-loha-event-id: evt_01J…
x-loha-event-type: com.loha.build.completed.v1
x-loha-delivery-id: whd_…
{
"specversion": "1.0",
"id": "evt_01J…",
"source": "/loha/pipeline-controller",
"type": "com.loha.build.completed.v1",
"subject": "run_…",
"time": "2026-06-17T04:12:30.000Z",
"datacontenttype": "application/json",
"data": {
"buildId": "run_…",
"workspaceId": "org_…",
"triggerId": "trg_…",
"status": "SUCCESS",
"durationSeconds": 142,
"finishedAt": "2026-06-17T04:12:30.000Z",
"repositoryId": "repo_…",
"repositoryName": "acme/api",
"runNumber": 412,
"branch": "main",
"commitSha": "9f2c1ab…"
}
}
Every field of data for com.loha.build.completed.v1:
| Field | Type | Notes |
|---|---|---|
buildId | string | The build (pipeline run). Also the CloudEvent subject. |
workspaceId | string | The owning workspace. |
triggerId | string | null | null if the trigger that fired the build was deleted before it finished. |
status | string | SUCCESS, FAILED, TIMEOUT or CANCELLED. |
durationSeconds | number | null | null if the build never started. |
finishedAt | string | ISO 8601. |
repositoryId | string | null | null for a manual run with no repository. |
repositoryName | string | null | owner/repo. null when repositoryId is. |
runNumber | number | null | Per-repository build number. null for a repo-less run. |
branch | string | null | null for a tag run or a manual run. |
commitSha | string | null | null when the run has no commit. |
Each one can be null, and the last five were added after the first release — an event
replayed from before that change legitimately lacks them. Treat a missing or null field
as "not known for this build" rather than an error, and don't key logic on a field being
present.
Use id (the CloudEvent id, echoed in x-loha-event-id) as your idempotency key:
delivery is at-least-once, so the same event id can arrive more than once.
Verify the signature
Every delivery carries x-loha-signature: sha256=<hex>, the HMAC-SHA256 of the raw
request body keyed with your webhook secret. Compute the same HMAC and compare in
constant time before trusting or parsing the payload:
import crypto from 'node:crypto'
// rawBody MUST be the exact bytes received, before JSON.parse.
function verifyLohaSignature(rawBody, header, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(header ?? '')
const b = Buffer.from(expected)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
If the signature doesn't match, return a non-2xx and drop the request. It isn't from Loha (or your secret is stale; see Rotate the secret).
Respond 2xx to acknowledge. Any other status (or a timeout) is treated as a failed delivery and retried.
Delivery, retries, and history
- Acknowledgement: a
2xxwithin 10 seconds marks the delivery successful. - Retries: failures retry with exponential backoff (1 min → up to 1 hour) for up to 10 attempts, then the delivery is parked as dead.
- At-least-once: dedupe on the CloudEvent
id. - History: see recent attempts (status, response code, error) under the webhook in
Settings → Webhooks, or via
GET /api/v1/webhooks/{id}/deliveries. Re-queue a delivery from the UI orPOST /api/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver. - Test it: Send ping (UI) or
POST /api/v1/webhooks/{id}/pingqueues a syntheticcom.loha.webhook.ping.v1delivery so you can confirm wiring before a real build runs. It is signed and shaped exactly like a real delivery, arrives whatever the subscription's event list says, and itsdatais{ "webhookId": "wh_…", "message": "Loha webhook ping" }— so a receiver that allowlists event types must include this one.
A webhook that fails persistently may be auto-disabled; re-enable it after fixing your endpoint.
Rotate the secret
Rotating issues a new secret (shown once) and deliveries are signed with it
immediately. Update your receiver promptly. There's no window where old signatures
keep arriving. Rotate from Settings → Webhooks or
POST /api/v1/webhooks/{id}/rotate-secret.
Security
- Endpoints must be public HTTPS. Loha refuses URLs that resolve to private, loopback, link-local, or cloud-metadata addresses (SSRF protection) and does not follow redirects.
- Secrets are encrypted at rest (AES-256-GCM) and never returned after creation.
- Creating, editing, rotating, and deleting webhooks is restricted to workspace admins/owners and is recorded in the audit log.
Permissions
| Action | Dashboard role | API scope |
|---|---|---|
| View webhooks and deliveries | member or higher | webhooks:read |
| Create / edit / rotate / delete | admin or owner | webhooks:write |