On this page

Automation

Webhooks

A webhook makes Wexa send a signed JSON POST to a URL you choose each time an ontology in your project is committed, updated or discarded.

Anyone who can reach the project's ontology management in the console can create one; the handler checks no role of its own, and which roles reach it is not stated in source. You manage webhooks in the console; no SDK method or public API route wraps them today.

Delivery contract

Method and bodyPOST, Content-Type: application/json, one event per request
HeadersX-Signature-SHA256 (the signature) and X-Wexa-Event (the event name)
Timeout15 seconds per delivery
Status to returnBelow 300 counts as success. Anything else, or no answer in time, is logged as a failure
RetriesNone. One attempt per event; see Retries
HTTPS onlyNot stated. Source does not check the URL's scheme; use HTTPS anyway

Wexa does not wait for your answer before carrying on. Delivery runs in the background after the change is saved, so a slow or failing receiver never blocks the edit that triggered it.

Create a webhook in the console

  1. In the console sidebar, open Connectors and choose the Ontology tab at the top of the page.
  2. In the ontology management bar, choose the Webhooks tab, then select Add Webhook.
  3. Optionally enter a Name; it defaults to Webhook.
  4. Enter the destination URL, for example https://example.com/hooks/fabric. It is required for every event except Inbound.
  5. Tick the events you want: Committed, Updated or Discarded. Committed is ticked by default. The Inbound option makes the webhook an ingest endpoint instead; that endpoint is not on the public gateway, so this page does not cover it.
  6. Leave Rate limit (per min) at its default of 60 unless you need otherwise. It applies only to inbound requests; deliveries to your URL are not throttled.
  7. Save. A green banner shows the secret, once. Copy it now: afterwards the row shows only its last four characters, as secret_hint.

Each webhook is a row in the Webhooks tab with an active or paused pill. Pause stops deliveries, Resume restarts them, and the bin icon deletes the webhook. Expand a row to see Recent deliveries: the event, status code, duration, any error and the time of each attempt.

Payloads

Every delivery has the same envelope. Keys arrive in alphabetical order, because the body is serialised from a map:

FieldMeaning
eventThe event name, also sent in X-Wexa-Event
project_idThe project the change belongs to
timestampWhen Wexa built the payload: UTC, RFC 3339, whole seconds
dataThe event fields below

ontology.committed

Sent the first time a proposal is committed as an ontology.

data fieldMeaning
proposal_id, proposal_nameThe proposal that was committed
ontology_idThe ontology it created
recommitfalse
nodes, relationshipsHow many of each the ontology has
{"data":{"nodes":12,"ontology_id":"ont_customers","proposal_id":"6650a1f2c3d4e5f607182930","proposal_name":"Customers","recommit":false,"relationships":9},"event":"ontology.committed","project_id":"proj_example","timestamp":"2026-11-03T09:30:00Z"}

ontology.updated

Sent from four places, and the data fields depend on which:

Causedata fields
A committed proposal is committed againproposal_id, proposal_name, ontology_id, recommit (true), nodes, relationships
An ontology is edited directlyontology_id, ontology_name, scope, nodes, relationships
A proposal is editedproposal_id, proposal_name
An edit is undoneontology_id, ontology_name, undo_of (the action that was undone)
{"data":{"ontology_id":"ont_customers","ontology_name":"Customers","scope":"project"},"event":"ontology.updated","project_id":"proj_example","timestamp":"2026-11-03T09:41:07Z"}

ontology.discarded

Sent when an ontology is deleted from the graph.

data fieldMeaning
ontology_id, ontology_nameThe ontology that was removed

ontology.inbound

Never delivered to you. Choosing it turns the webhook into an ingest endpoint instead, which this page does not cover.

Verify a delivery

Wexa signs the exact bytes of the request body with HMAC-SHA256, keyed with your webhook's secret, and sends the result as:

X-Signature-SHA256: sha256=<64 hex characters>

The key is the secret string as shown, all 64 characters used as text, not decoded from hex. Compute the digest over the raw body before you parse it: re-serialising JSON changes the bytes, and Wexa's serialiser escapes <, > and &.

The signature carries no timestamp and Wexa states no tolerance window. The timestamp field is inside the signed body, so it cannot be altered without breaking the signature; whether to reject old ones, and how old, is your policy. Wexa also sends no delivery id, so you cannot de-duplicate on one.

Test vector. With secret 9f2c41d7b8a35e60c1d4f7a92b3e8c05a6d1f4e7b2c93a58d0e6f1b7c4a29d83 and the ontology.committed body above, the header is sha256=26672933a945a5ba051f27f73b07bef24353785811b1922866a6b95c10f36779.

# BODY is the request body exactly as received; SECRET is the webhook secret.
printf %s "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/.*= /sha256=/'
# Compare the output with the X-Signature-SHA256 header.

The shell comparison is not constant-time; use it for testing only, and use the Python or TypeScript version in a receiver.

Respond with any status below 300 once the signature checks out, and answer a bad signature with 401; Wexa only records the code.

Retries

Wexa does not retry. Each event is delivered once, and a failure is written to the webhook's delivery log (status code, duration and error) and nothing else; there is no schedule, no backoff and no dead-letter queue. The log keeps the latest 500 deliveries per webhook. If your receiver was down, read the log, then re-read the ontology; events are not replayed.

Limits and behavior

Fan-out budgetOne 30-second budget covers every webhook an event is delivered to, so a slow receiver can delay the ones after it
Delivery orderNot guaranteed; each event is delivered independently
Secret64 hex characters, shown once; no rotation is offered
Delivery logThe latest 500 per webhook are kept; the console shows the newest 100
PauseA paused webhook receives nothing
Callbacks Wexa receives

Callbacks Wexa receives

Nine other routes on the gateway exist to be called by GitHub, OpenMetadata, Unipile, Bland and fal.ai. None takes an Authorization: Bearer credential, because none of those senders holds one, so how each is authenticated, and where, differs.

RouteAuthenticated byChecked by
POST /v1/codesync/webhook/githubHMAC-SHA256 over the raw body, X-Hub-Signature-256the gateway
POST /v1/catalog/om/webhookHMAC-SHA256 over the raw body, X-OM-Signaturethe gateway
POST /v1/internal/catalog/ingestshared x-server-key headerthe gateway
POST /actions/linkedin/notify/{connectorID}/{pin}an unguessable {pin} segmentdownstream, not the gateway
POST /actions/mail/notify/{connectorID}/{pin}an unguessable {pin} segmentdownstream, not the gateway
POST /actions/whatsapp/notify/{connectorID}/{pin}an unguessable {pin} segmentdownstream, not the gateway
POST /unipile-webhook/{projectID}/{connectorID}nothingdownstream, by re-fetching
POST /blandai/{triggerID}nothingdownstream, by trigger lookup
POST /falai/{triggerID}nothingdownstream, by trigger lookup
  • GitHub. The gateway recomputes sha256=<hex> over the bytes it received, before parsing, and compares in constant time. A mismatch or missing header is 401 {"error":"invalid_signature","error_description":"GitHub webhook signature mismatch"}; a non-push event is 200 ignored; a push for an unbound repository is 404 repo_not_bound; with no secret configured, unsigned requests are accepted. What a push does is on Code sync.
  • OpenMetadata. The body is one event or an array. The gateway answers 202 {"status":"accepted"} before any synchronisation, coalesces a burst into one run, and ignores identity and operations noise (users, teams, bots, roles, policies, test cases). 503 means the handler is not configured on that deployment.
  • Internal catalog ingest. Service-to-service, authenticated by the x-server-key header. A missing and a wrong key both give 401 {"error":"unauthorized"}. Call POST /v1/catalog/ingest with a credential instead; see Catalog.
  • Notify, Unipile, Bland and fal.ai routes. The gateway checks the path prefix, passes the body through byte for byte, copies Content-Type and copies no other inbound header, so an Authorization header sent here is dropped. The upstream status is mirrored back; an unreachable upstream is 502 upstream_unreachable. Authenticity is established downstream: hosted authentication re-fetches the reported account from Unipile, the Unipile webhook receiver re-reads the connector's trigger configuration, and the Bland and fal.ai receivers look the trigger id up, answering 404 for an unknown one.

On the routes without a gateway check, anyone who can reach your gateway can send a request and the gateway will forward it. None of these routes returns your data, and none is a general proxy: each re-checks its path prefix before forwarding.

What's next