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 body | POST, Content-Type: application/json, one event per request |
| Headers | X-Signature-SHA256 (the signature) and X-Wexa-Event (the event name) |
| Timeout | 15 seconds per delivery |
| Status to return | Below 300 counts as success. Anything else, or no answer in time, is logged as a failure |
| Retries | None. One attempt per event; see Retries |
| HTTPS only | Not 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
- In the console sidebar, open Connectors and choose the Ontology tab at the top of the page.
- In the ontology management bar, choose the Webhooks tab, then select Add Webhook.
- Optionally enter a Name; it defaults to
Webhook. - Enter the destination URL, for example
https://example.com/hooks/fabric. It is required for every event except Inbound. - 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.
- Leave Rate limit (per min) at its default of
60unless you need otherwise. It applies only to inbound requests; deliveries to your URL are not throttled. - 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:
| Field | Meaning |
|---|---|
event | The event name, also sent in X-Wexa-Event |
project_id | The project the change belongs to |
timestamp | When Wexa built the payload: UTC, RFC 3339, whole seconds |
data | The event fields below |
ontology.committed
Sent the first time a proposal is committed as an ontology.
data field | Meaning |
|---|---|
proposal_id, proposal_name | The proposal that was committed |
ontology_id | The ontology it created |
recommit | false |
nodes, relationships | How 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:
| Cause | data fields |
|---|---|
| A committed proposal is committed again | proposal_id, proposal_name, ontology_id, recommit (true), nodes, relationships |
| An ontology is edited directly | ontology_id, ontology_name, scope, nodes, relationships |
| A proposal is edited | proposal_id, proposal_name |
| An edit is undone | ontology_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 field | Meaning |
|---|---|
ontology_id, ontology_name | The 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 budget | One 30-second budget covers every webhook an event is delivered to, so a slow receiver can delay the ones after it |
| Delivery order | Not guaranteed; each event is delivered independently |
| Secret | 64 hex characters, shown once; no rotation is offered |
| Delivery log | The latest 500 per webhook are kept; the console shows the newest 100 |
| Pause | A 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.
| Route | Authenticated by | Checked by |
|---|---|---|
POST /v1/codesync/webhook/github | HMAC-SHA256 over the raw body, X-Hub-Signature-256 | the gateway |
POST /v1/catalog/om/webhook | HMAC-SHA256 over the raw body, X-OM-Signature | the gateway |
POST /v1/internal/catalog/ingest | shared x-server-key header | the gateway |
POST /actions/linkedin/notify/{connectorID}/{pin} | an unguessable {pin} segment | downstream, not the gateway |
POST /actions/mail/notify/{connectorID}/{pin} | an unguessable {pin} segment | downstream, not the gateway |
POST /actions/whatsapp/notify/{connectorID}/{pin} | an unguessable {pin} segment | downstream, not the gateway |
POST /unipile-webhook/{projectID}/{connectorID} | nothing | downstream, by re-fetching |
POST /blandai/{triggerID} | nothing | downstream, by trigger lookup |
POST /falai/{triggerID} | nothing | downstream, 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 is401 {"error":"invalid_signature","error_description":"GitHub webhook signature mismatch"}; a non-push event is200 ignored; a push for an unbound repository is404 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).503means the handler is not configured on that deployment. - Internal catalog ingest. Service-to-service, authenticated by the
x-server-keyheader. A missing and a wrong key both give401 {"error":"unauthorized"}. CallPOST /v1/catalog/ingestwith 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-Typeand copies no other inbound header, so anAuthorizationheader sent here is dropped. The upstream status is mirrored back; an unreachable upstream is502 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, answering404for 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
- Schema and ontology: the ontologies whose changes these webhooks report.
- Authentication: how every other route is authenticated, and why these nine are the exception.
- Errors and retries: what to do when a call you make fails.