Webhooks

Mnemexa delivers HMAC-SHA256-signed HTTP callbacks to your URL when workspace events occur — lifecycle transitions, billing events, and detected usage anomalies. Endpoints are configured per workspace from the dashboard.

Configuring an endpoint

Webhook endpoints are managed in the dashboard, not through the public API. Go to Settings → Webhooks in the workspace and:

  1. Add an HTTPS URL that will receive the callbacks.
  2. Optionally filter to a subset of event types (default: all events).
  3. Copy the signing secret. The secret is shown once and used to verify request authenticity.

A signing secret looks like:

whsec_a1b2c3d4e5f6g7h8…

Treat it with the same care as an API key — anyone with the secret can forge a webhook request that looks legitimate.

Event types

These are the event types that may be delivered to customer webhooks. Internal admin.* events are not delivered.

Event typeTriggered by
workspace.status_changedWorkspace transitions between active, suspended, limit_reached, pending_payment.
billing.payment_confirmedA payment was successfully recorded against the workspace.
billing.renewal_successA subscription renewed for another billing cycle.
billing.payment_failed_enhancedA payment attempt failed. Carries the failure reason.
payment.failedCoarser-grained payment failure signal (legacy; kept for backward compat).
anomaly.detectedThe anomaly scanner flagged unusual usage on the workspace.
webhook.testA manual test delivery initiated from the dashboard “Send test” button.

If you filter to specific event types when configuring the endpoint, only those types are delivered. The default (no filter) sends every event the workspace produces.

Delivery format

The body is application/json. Shape:

{
  "event_id": "evt_…",
  "event_type": "workspace.status_changed",
  "workspace_id": 42,
  "created_at": "2026-05-15T18:30:00Z",
  "payload": { /* event-specific fields */ }
}

The payload shape depends on event_type. Build your handler against event_type as the discriminator.

Headers

Every delivery includes:

HeaderPurpose
X-BizX-Event-IdUnique event ID. Use to deduplicate replays.
X-BizX-Event-TypeThe event type, e.g. workspace.status_changed. Same as in the body.
X-BizX-TimestampUnix epoch seconds. Must fall within a 5-minute window of receipt to count as fresh.
X-BizX-Signaturev1=<hex-digest> HMAC-SHA256 of {timestamp}.{raw-body}. See verification below.

Header names carry the X-BizX- prefix for historical reasons (Mnemexa rebranded from BizXEngine in 2026-05). They are stable and will not be renamed; do not depend on a future X-Mnemexa- alias.

Verifying signatures

The signature scheme:

signed_message = "{X-BizX-Timestamp}.{raw_request_body}"
expected_sig   = "v1=" + hex(hmac_sha256(signing_secret, signed_message))

Reject the request if:

  1. X-BizX-Timestamp is more than 300 seconds (5 minutes) older or newer than the current time — this defends against replay.
  2. The computed expected_sig doesn’t match X-BizX-Signature (use constant-time comparison).

Python

import hmac, hashlib, time

def verify(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    if abs(int(time.time()) - int(timestamp)) > 300:
        return False
    message = f"{timestamp}.{body.decode('utf-8')}"
    expected = "v1=" + hmac.new(
        secret.encode("utf-8"),
        message.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, timestamp, rawBody, signature) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const message = `${timestamp}.${rawBody}`;
  const expected = "v1=" + createHmac("sha256", secret).update(message).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Verify against the raw request body, not a JSON-parsed-then-re-serialized version. Re-serializing changes key order and whitespace, breaking the signature.

Retry policy

If your endpoint doesn’t respond with a 2xx status within the timeout, Mnemexa retries with exponential backoff up to 7 attempts per event. After the 7th failure, the delivery is marked permanently failed and surfaced in the dashboard’s webhook deliveries view.

Successful deliveries are deduplicated by event_id on Mnemexa’s side — but you should still implement idempotency on your end using the X-BizX-Event-Id header in case a retry crosses with a delayed 2xx.

Inspecting deliveries

The workspace dashboard’s Settings → Webhooks → Deliveries tab shows every delivery attempt with status, response code, and body. From there you can also replay a failed delivery.

Errors and edge cases

  • If your endpoint returns 4xx, the delivery is marked permanently failed without further retries (4xx means “you don’t want this”, from the system’s perspective).
  • 5xx and timeouts trigger the retry policy described above.
  • Endpoint URLs must be HTTPS. Plain HTTP is rejected at configuration time.
  • Maximum body size: ~256 KB (most events are well under 4 KB).