Webhooks
Send selected repository, pull-request, pipeline, work, and package events to your HTTPS service, then verify and process every delivery without trusting the network.
Own both ends of the delivery.
Prerequisites
- A verified Mozaic account and membership in the organization.
- An existing project. Webhooks created in the current workspace are project scoped.
- The claim at the project or organization scope.
- A fresh sign-in to create, update, rotate, deactivate, or manually redeliver.
- A publicly resolvable HTTPS endpoint that accepts POST requests and can preserve the exact request body for signature verification.
The webhook is governed in the project whose events it exports; the project-scoped Manage projects claim controls this workflow.
Build the endpoint before creating the webhook.
- Expose one HTTPS route.Use a dedicated path such as
/hooks/mozaicwith a certificate trusted by normal public HTTPS clients. - Read the raw body once.Signature verification covers the exact bytes sent. Do not parse, reformat, transcode, or reconstruct JSON before computing the HMAC.
- Persist an idempotency key.Use
event_idas a unique processing key. A manual redelivery creates a new delivery but carries the same event. - Acknowledge quickly.Verify, durably enqueue or record the event, and return a 2xx response. Perform slow integration work after acknowledgement.
- Keep a safe diagnostic trail.Record the delivery ID, event ID, event type, result, and processing time without logging the signing secret or sensitive payload unnecessarily.
The dispatcher does not reach loopback, local, private, link-local, carrier-grade NAT, documentation, or other reserved addresses. Use a public ingress that applies your own authentication and routing controls.
Create a project webhook.
- Choose Endpoint.From an empty state the equivalent action is Create project endpoint. Enter a recognizable name, such as
Release index
orDeployment events
. - Enter the destination.Use an HTTPS URL on a public DNS hostname. URLs containing credentials, fragments, IP literals, localhost, or private destinations are rejected.
- Select events.Choose at least one exact event type from the supported list. Send only the data the receiver needs.
- Create the endpoint.Mozaic returns a signing secret beginning with
mwhsec_. - Copy the secret immediately.Store it in the receiver's secret manager. Mozaic derives the signing value when dispatching and does not expose it again.
Success state
The webhook appears as Active with its selected filters. The next matching project event creates an Initial delivery and POSTs one signed JSON envelope to the destination.
Names are limited to 120 characters. Destination URLs are limited to 2,048 characters. Updating an endpoint changes future deliveries only; existing delivery records retain their original evidence.
Repository and package moves change the project scope used for delivery, diagnostics, and manual redelivery. Disable affected source-project webhooks before moving, wait for admitted outgoing attempts to finish, and explicitly review destination endpoints. Reenabling an old endpoint does not restore access to moved events. Event envelopes keep their original project evidence; historical pipeline events keep the run's execution project. A retained event identifier or payload project is not current delivery authority.
Subscribe to exact, versioned event names.
| Area | Supported event types |
|---|---|
| Repository | repository.created, repository.imported, repository.push, repository.archived, repository.reactivated, repository.exported, repository.deleted, repository.published |
| Pull request | pull_request.opened, pull_request.updated, pull_request.merged |
| Pipeline | pipeline.started, pipeline.succeeded, pipeline.failed |
| Work item | work_item.created, work_item.updated, work_item.transitioned |
| Package | package.published, package.symbols_published, package.unlisted, package.relisted, package.deprecation_changed, package.purged |
Filters are an allowlist. A new product event is not sent to an existing endpoint unless its exact type is selected. Treat the envelope's schema_version and event_type as protocol values, and ignore additive JSON properties your version does not use.
Use headers for delivery context and JSON for domain data.
Request headers
| Header | Meaning |
|---|---|
Content-Type | application/json. |
User-Agent | Mozaic-Webhook/1. |
X-Mozaic-Webhook-Id | The configured endpoint UUID. |
X-Mozaic-Delivery-Id | The UUID for this exact attempt record. |
X-Mozaic-Event | The lowercase event type, matching the envelope. |
X-Mozaic-Delivery-Timestamp | The dispatch time in UTC RFC 3339 form, to whole seconds. |
X-Mozaic-Signature-256 | sha256= followed by the lowercase hexadecimal HMAC-SHA-256 of the exact body. |
Version-one event envelope
- Identity
event_id,organization_id,placement_generation,event_type, andschema_version.- Subject and actor
resource,actor, andvisibilitypreserve the organization-bound subject and originator.- Ordering and tracing
aggregate_sequence,correlation_id, optionalcausation_id, andoccurred_at.- Event data
payloadis an event-specific JSON object, bounded to 64 KiB.
Aggregate sequence helps order events for one resource; it is not a global ordering guarantee. Correlation and causation values connect related operations but do not replace receiver idempotency.
Authenticate the exact bytes before parsing.
The signing key is the complete show-once mwhsec_... string encoded as UTF-8. Compute HMAC-SHA-256 over the raw body, prefix the lowercase hex digest with sha256=, and compare in constant time.
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
app.MapPost("/hooks/mozaic", async (HttpRequest request, IConfiguration config) =>
{
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer);
var body = buffer.ToArray();
var secret = config["Mozaic:WebhookSecret"];
var supplied = request.Headers["X-Mozaic-Signature-256"].ToString();
var timestampText = request.Headers["X-Mozaic-Delivery-Timestamp"].ToString();
if (string.IsNullOrEmpty(secret) ||
!DateTimeOffset.TryParse(timestampText, CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal, out var timestamp) ||
Math.Abs((DateTimeOffset.UtcNow - timestamp).TotalMinutes) > 5)
{
return Results.Unauthorized();
}
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expectedText = "sha256=" +
Convert.ToHexString(hmac.ComputeHash(body)).ToLowerInvariant();
var expected = Encoding.ASCII.GetBytes(expectedText);
var actual = Encoding.ASCII.GetBytes(supplied);
if (expected.Length != actual.Length ||
!CryptographicOperations.FixedTimeEquals(expected, actual))
{
return Results.Unauthorized();
}
using var envelope = JsonDocument.Parse(body);
var eventId = envelope.RootElement.GetProperty("event_id").GetGuid();
// Insert eventId with a unique constraint and enqueue work atomically.
return Results.NoContent();
});
- Reject missing context.Require the signature, timestamp, delivery ID, and event header.
- Bound replay.Parse the delivery timestamp and accept a small clock-skew window appropriate to your ingress. The example uses five minutes.
- Compute the HMAC.Use the full secret and exact body bytes.
- Compare in constant time.Reject different lengths and invalid digests before parsing or acting on the event.
- Validate the envelope.Require schema version 1, a known event type, and an expected project or organization binding before dispatching application logic.
Some middleware consumes or normalizes the request body before a route handler runs. Enable raw-body buffering at the ingress boundary and verify against those original bytes.
Make every handler idempotent.
- Start a local transaction.Insert
event_idinto a receipt table with a unique constraint and enqueue or record the intended work in the same transaction. - Treat duplicates as success.If the event ID already exists, return 2xx without repeating the side effect. A manual redelivery is expected to reach this path.
- Return before slow work.The entire HTTP attempt has a 15-second timeout. A durable queue or outbox lets your business action continue independently.
- Handle each event version explicitly.Route on both
event_typeandschema_version; log and safely acknowledge or quarantine unsupported versions according to your policy. - Use resource sequence carefully.For state projections, retain the highest accepted
aggregate_sequenceper resource and reconcile gaps from Mozaic instead of assuming global delivery order.
Success state
The receiver verifies the delivery, durably records the event exactly once, returns a 2xx response within the timeout, and processes the downstream action independently.
Inspect the exact attempt before redelivering.
- Pending
- The delivery is recorded and waiting to be leased by the dispatcher.
- In flight
- One dispatcher owns the active HTTP attempt.
- Succeeded
- The receiver returned any 2xx status.
- Failed
- The dispatcher completed the attempt and received a non-2xx response or rejected the destination.
- Unknown
- The request or response did not complete with enough evidence to know whether the receiver acted, such as a transport failure or interrupted response read.
Mozaic makes one automatic attempt. It does not automatically retry Failed or Unknown deliveries. The record includes event type, kind, status, timing, HTTP status when known, a bounded error, and up to 4,096 bytes of response excerpt.
Manual redelivery
- Open the terminal delivery.Inspect its status, response, receiver logs, and event ID.
- Fix or confirm the receiver.For Unknown, first check whether the original event was already recorded.
- Choose Redeliver.Fresh authentication and Manage projects are required.
- Follow the new record.Mozaic creates a distinct Manual redelivery linked to the original; it never rewrites the Initial attempt.
The receiver may have completed the side effect before the connection failed. Deduplicate by event ID, then redeliver only after the endpoint can return a deterministic 2xx response.
Rotate without leaving stale trust behind.
Rotate a signing secret
- Open the webhook settings.Confirm the endpoint identity and coordinate a short receiver change window.
- Choose Rotate secret.Fresh authentication is required. Copy the new
mwhsec_...value from the show-once response. - Update the receiver immediately.The previous secret becomes invalid as soon as rotation succeeds; there is no dual-secret overlap.
- Verify the next delivery.Confirm the endpoint accepts the new signature and no secret appears in logs.
Deactivate an endpoint
Deactivate the webhook to stop future matching events from creating deliveries. Existing endpoint metadata and delivery history remain available as evidence. Reactivate only after the destination and secret configuration are ready again.
Design inside the outbound boundary.
| Boundary | Current behavior |
|---|---|
| Transport | HTTPS only; direct connection with no proxy and no redirects. |
| Destination | Public DNS hostname only. Credentials, fragments, IP literals, localhost, private, local, multicast, and reserved ranges are rejected. |
| DNS | Revalidated at delivery time and pinned to a permitted public address for that request. |
| Connect timeout | 5 seconds. |
| Total request timeout | 15 seconds. |
| Success | Any HTTP 2xx response. |
| Automatic attempts | One; retry is an explicit manual redelivery. |
| Event payload | JSON object, 64 KiB maximum inside the version-one envelope. |
| Stored diagnostics | Response excerpt up to 4,096 bytes; error text up to 1,000 characters. |
| Collection page size | Up to 100 endpoints or deliveries per page. |
- Treat the signing secret like a password. Store it in a secret manager and grant the receiver process read access only.
- Signature verification authenticates content; it does not make every event appropriate for every downstream action. Enforce your own resource and event allowlists.
- Return generic, bounded error bodies. Response excerpts are retained in Mozaic and should not contain credentials or sensitive internal diagnostics.
- Expect additive payload fields. Fail closed on unsupported schema versions, but avoid breaking on fields your handler does not consume.
Call scoped APIs with a session-bound human token.
Create a personal access token in the target tenant workspace under Settings › Access › API tokens. The token is bound to that one organization and its human owner. Choose only when the integration must list or create projects, and only when it must manage project work. Choose to list, create, or import repositories and list or register named pipeline definitions. This scope does not issue Git credentials, execute pipelines, delete configuration, or publish packages.
Issue PATs from a personal-account organization session with fresh local MFA. Company/provider-only sessions cannot issue PATs. Select Issue API token to review the exact organization, expiry and requested scopes. Confirming that review opens the required MFA prompt automatically; complete the passkey or authenticator challenge to continue issuance. Cancelling the review or challenge does not issue a token. Each scope shows its claim name separately from its description.
A PAT retains human purpose and its exact issuing tenant session. Every request must satisfy the current action policy and session evidence; logout, session rotation or loss of required proof can stop access before token expiry. A PAT never receives a machine-policy exception. Repository or package credentials delegated through it retain human provenance and their own scopes, expiry and revocation controls. Previously issued PATs without session evidence cannot satisfy actions that require that evidence. Use an explicitly supported machine-purpose credential for unattended Git or package access where the organization policy permits it.
Integrations can link to the target organization workspace at /settings/access/api-tokens?organization=<organization-uuid>&scopes=manage_source,run_pipelines,manage_packages&name=Migration&expires_in_days=1. Use the workspace's own tenant origin and immutable organization ID. The link preselects a request for the user to inspect and approve; it never issues a token automatically. Only the four shown query parameters are accepted. Requested scopes must be supported, and the organization must match the workspace. Duplicate parameters, callbacks, redirect destinations and token parameters are rejected. Never put credentials or confidential data in the URL. The issued token appears only once after approval and must be transferred through a protected channel.
| Selected scope | Supported version-one API operations |
|---|---|
| Provision source | GET and POST /api/v1/organizations/{organization_id}/projects/{project_id}/repositories; POST /api/v1/organizations/{organization_id}/projects/{project_id}/repositories:import (multipart bundle upload); GET and POST /api/v1/organizations/{organization_id}/repositories/{repository_id}/pipelines. |
| Provision packages | POST /api/v1/organizations/{organization_id}/projects/{project_id}/package-registries (OCI only); GET and POST /api/v1/organizations/{organization_id}/package-registries/{registry_id}/repositories; POST /api/v1/organizations/{organization_id}/package-registries/{registry_id}/credentials (explicit-repository OCI pull only, expiry capped to PAT). |
| Provision source + Authorize pipeline triggers | POST /api/v1/organizations/{organization_id}/repositories/{repository_id}/credentials issues a repository-scoped Git credential capped to PAT expiry. Both delegations and current source/run authority are required. Use the credential for a real governed Git update; manual runs retain restricted trust. |
| Manage projects | GET and POST /api/v1/organizations/{organization_id}/projects. |
| Manage work |
|
- Keep the target explicit.Use the matching tenant hostname and verify the organization ID before any mutation. A token for one organization is never accepted for another.
- Send the bearer in one header.Use
Authorization: Bearer <token>. Do not place the value in a URL, source file, payload, work item, log, status file, or command transcript. - Use protocol controls.Supply
Idempotency-Keywhere the endpoint supports it, and use the currentETagwithIf-Matchfor version-checked assignment or relation changes. - Reconcile uncertainty.If a non-idempotent Work mutation loses its response, read current server state before deciding whether to repeat it. Never infer that a timeout means the mutation did not commit.
- Close one-off access.Verify counts and relationships, then revoke the token with fresh MFA. Expiry, revocation, issuing-session logout or rotation, membership removal, action-policy changes and live role changes take effect without waiting for the token lifetime.
An API bearer is non-cookie authentication, so it does not use CSRF. The tenant web interface still requires its secure session cookie, exact-origin and fetch-metadata checks, and a CSRF token. A bearer cannot authenticate Git, LFS, NuGet, operator, account, realtime, or any unlisted API route.
Resolve common integration failures.
The destination is rejected during setup
Use an https:// URL with a public DNS hostname and no embedded username, password, fragment, or IP literal. Local tunnel and documentation-range addresses are intentionally blocked.
Signature verification always fails
Use the complete mwhsec_... value as UTF-8 HMAC key, hash the exact raw request bytes, emit lowercase hexadecimal with the sha256= prefix, and compare in constant time. Check whether middleware changed or consumed the body.
The delivery is Failed with a redirect status
Mozaic does not follow redirects. Configure the final HTTPS destination directly and make the route return 2xx after durable acceptance.
The delivery is Unknown
The dispatcher cannot prove whether the receiver acted. Look up event_id in the receiver's idempotency store before choosing Redeliver; never infer failure solely from Unknown.
The endpoint times out
Respond within 15 seconds. Move downstream API calls, builds, or data synchronization behind a durable local queue and acknowledge only after the event has been recorded safely.
No delivery appears for an expected action
Confirm the endpoint is Active, the exact event type is selected, and the action occurred in this endpoint's project. Existing filters do not automatically include newly introduced event types.
Deliveries fail immediately after secret rotation
Rotation invalidates the old secret immediately. Install the new show-once value in the receiver, then manually redeliver affected terminal records after idempotency checks.
An API bearer returns 401 or 403
Verify the tenant hostname, token expiry, selected scope, supported route, and the owner's current organization membership and effective role. Revoke and replace a suspected token through the browser; never send the raw bearer to an operator for diagnosis.