Exact control-plane facts, each with an explicit aggregate boundary.
Ship a receiver that survives production.
A complete, implementation-ready contract for raw-body verification, destination ownership, durable deduplication, ordered delivery, bounded retries, and governed replay—plus copy-ready TypeScript, Python, Java, and Go receivers.
Raw-byte HMAC examples with constant-time comparison.
Reject stale delivery timestamps before business work.
The guide states exactly what its evidence can support.
Console events—not product webhooks.
These four topics describe this developer console reference implementation. They are not Mappls product webhooks, InTouch payloads, or the 138 application-owned event blueprints.
Verify first. Commit once. Acknowledge last.
The secure path is deliberately short. Each step closes one failure mode without assuming exactly-once transport.
- 01
Bound
Reject a body above 256 KiB and retain the exact raw bytes.
- 02
Verify
Validate timestamp, event ID, signature shape, and constant-time HMAC before parsing.
- 03
Parse
Decode JSON and allow-list the exact versioned topic and event schema.
- 04
Deduplicate
Insert the event ID into a unique inbox record in the same transaction as the business effect.
- 05
Commit
Persist the normalized effect, evidence, and any application outbox work atomically.
- 06
Acknowledge
Return 2xx only after commit; otherwise return a deliberate non-2xx for bounded retry.
import { createHmac, timingSafeEqual } from "node:crypto";
const MAX_AGE_SECONDS = 300;
export function verifyMapplsWebhook(input: {
rawBody: Buffer;
headers: Record<string, string | undefined>;
secret: string;
nowSeconds?: number;
}) {
const timestamp = input.headers["x-mappls-timestamp"] ?? "";
const eventId = input.headers["x-mappls-event-id"] ?? "";
const signature = input.headers["x-mappls-signature"] ?? "";
const now = input.nowSeconds ?? Math.floor(Date.now() / 1000);
if (!/^\d{10}$/.test(timestamp) || Math.abs(now - Number(timestamp)) > MAX_AGE_SECONDS) {
throw new Error("stale_webhook");
}
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/.test(eventId)) {
throw new Error("invalid_event_id");
}
if (!/^v1=[a-f0-9]{64}$/.test(signature)) throw new Error("invalid_signature");
const expected = createHmac("sha256", input.secret)
.update(`${timestamp}.${eventId}.`)
.update(input.rawBody)
.digest();
const actual = Buffer.from(signature.slice(3), "hex");
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
throw new Error("invalid_signature");
}
return JSON.parse(input.rawBody.toString("utf8"));
}
export function verificationResponse(secret: string, challengeId: string, nonce: string) {
return `v1=${createHmac("sha256", secret)
.update(`${challengeId}.${nonce}`)
.digest("hex")}`;
}
// Commit event.id and your durable effect in one transaction.
// Return 2xx only after that transaction commits.Authenticate the exact bytes you received.
Do not parse, reserialize, trim, or otherwise normalize the body before computing the digest.
Signed bytes
${timestamp}.${eventId}.${rawBody}HMAC-SHA256 · signature v1=<hex> · raw body limit 256 KiB
- Read and bound the raw request bytes before JSON parsing.
- Reject timestamps outside the five-minute window before doing business work.
- Recompute the HMAC over the exact timestamp, event ID, separators, and raw body.
- Compare digests in constant time and reject unknown signature versions.
- Keep endpoint signing secrets in a receiver-side secret manager; never log or return them.
Delivery headers
content-type- application/json
x-mappls-event-id- Stable delivery identity and receiver deduplication key
x-mappls-event-type- Versioned topic identity
x-mappls-timestamp- Unix seconds included in the signature
x-mappls-signature- v1=<lowercase HMAC-SHA256 hex>
x-mappls-test-mode- true only for console-generated synthetic tests
Prove the receiver before opening business delivery.
A configured endpoint receives a signed webhook.endpoint_verification.v1 challenge. Return x-mappls-verification-response: v1=<lowercase HMAC-SHA256 hex> within 15 minutes.
Make failure observable and recovery deliberate.
Retry
Network errors, timeouts, and non-2xx responses use bounded Retry-After or exponential backoff until terminal exhaustion.
Ordering
The worker dispatches only the earliest unresolved delivery in each endpoint + aggregate type + aggregate ID lane. Independent lanes continue in parallel.
Deduplication
Create a unique durable inbox record for every event ID in the same transaction as the business effect.
Governed replay
Replay preserves the original event ID and aggregate sequence; it is another attempt at the same transition. Never bypass receiver deduplication for a replay. Reconciliation and replay approval are operational evidence, not a new event.
Four versioned control-plane topics.
Subscribe only to topics your receiver can validate and handle idempotently.
application.provisioning_requested.v1Application provisioning requested
The control plane durably accepted a provisioning request and queued the provider handoff. The application is not active yet.
Ordering aggregate · applicationapplication.provisioned.v1Application provisioned
A separately authenticated provider result activated the requested application identity. Consumers should still read current entitlement and credential state.
Ordering aggregate · applicationusage.metered.v1Usage metered
An authoritative meter event was accepted for one application operation. Use it as a notification, not as a replacement for billing reconciliation.
Ordering aggregate · applicationusage.threshold_reached.v1Usage threshold reached
A configured monthly usage alert crossed its threshold for one product and application. Re-read current usage before taking consequential action.
Ordering aggregate · usage_alertEvery transition leaves evidence.
- 1configured
Endpoint and exact topic set recorded; signing secret revealed once.
- 2verification pending
Signed ownership challenge queued; business delivery remains closed.
- 3verified + active
Synthetic tests and subscribed business fan-out may be admitted.
- 4queued
Immutable event identity and aggregate lane allocated transactionally.
- 5delivering
One fenced worker lease owns the bounded attempt.
- 6delivered / retrying / failed
Safe attempt evidence records success, retry schedule, or terminal exhaustion.
- 7replay governed
Paused-only request, independent decision, single-use authorization, and preserved identity.
Do not stop at a successful curl.
Ship only after the receiver, its secret boundary, failure paths, operational evidence, and recovery runbook have all been exercised.
Review trust boundaries- Public HTTPS receiver on standard port 443 with no redirect dependency
- Raw-body capture and 256 KiB limit before parsing
- Five-minute timestamp rejection and constant-time HMAC comparison
- Endpoint-specific signing secret stored in a managed secret system
- Unique durable inbox constraint on event ID
- Inbox record, domain effect, audit, and application outbox committed atomically
- Versioned schema allow-list and unknown-topic rejection
- Fast 2xx after commit; slow work moved to an application queue
- Synthetic challenge and subscribed-topic tests completed
- Duplicate, stale, malformed, timeout, 429, 5xx, ordering, and replay tests
- Metrics for latency, signature rejection, duplicates, retries, lane blocking, and terminal failures
- Runbook for pause, drain, secret rotation, reconciliation, approved replay, and rollback