Connect a project with signed 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.
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.
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.