What are webhooks?
Webhooks are HTTP callbacks that deliver data to external systems in real-time when an event occurs within Infrahub. They allow you to:
- Notify external systems about changes in your infrastructure (for example new configuration generated)
- Trigger automated workflows in CI/CD pipelines
- Keep other tools and services synchronized with your infrastructure data
- Build custom integrations with your existing toolchain
The Webhook System also powers the Activity Log.
Configuring webhooks
Infrahub supports two different types of webhooks:
Standard Webhooksends event information to an external endpoint.Custom Webhookallows the ability to tie events to a Transformation to configure the payload being sent to an external endpoint.
Custom webhooks might be useful in situations where the endpoint needs a specific payload to be successful. A use-case that might need this would be triggering external actions in platforms like GitHub and GitLab.
The data passed into a normal Python Transformation will typically be the result of the GraphQL query, but webhooks execute the Transformation by passing in the webhook data instead.
Currently, it is required to define a GraphQL query to be defined on a Transformation regardless of whether it is used for a custom webhook or not. In the future, this will not be required.
Webhooks can be configured to trigger on specific event types, such as:
infrahub.branch.mergedinfrahub.node.updated
This allows you to tailor webhooks for specific use cases.
Configuring branch types
Webhooks can also be configured to trigger based on branch types:
- Other Branches – All branches except the default branch.
- Default Branch – Only the default branch.
- All Branches – Every branch.
This flexibility ensures that webhooks only trigger when relevant, depending on the use case. For example, you might configure a webhook to notify an external system only when a user makes changes directly to the default branch.
Node kind
Node kinds can be configured to only trigger webhooks for specific nodes. For example only send webhooks for the node kind of InfraDevice.
Node kind can only be used where node_kind is True in Infrahub events. Below are some example events that are enabled:
infrahub.node.createdinfrahub.node.updatedinfrahub.node.deletedinfrahub.artifact.createdinfrahub.artifact.deletedinfrahub.group.member_addedinfrahub.group.member_removedinfrahub.validator.member_removed
Webhook configuration details
- Description: A description can be set to identify the webhooks purpose.
- URL: The destination URL can be configured, such as
http://ansible-eda:8080. - Transformation (Custom Webhook only): The Python Transformation used to customize the webhook payload before being sent to the webhook receiver.
Shared key for security
A shared key can be configured to sign webhook requests. This key ensures the authenticity and integrity of the webhook message.
When a webhook is sent:
- The sender generates a signature using the shared key.
- The signature is included in the request headers (
webhook-signature). - The message ID is included in the request headers (
webhook-id). - The timestamp is included in the request headers (
webhook-timestamp).
The receiver then uses the same shared key to verify the signature, confirming that the message has not been tampered with and that it is from a trusted source.
Python FastAPI Example
import base64
import hashlib
import hmac
import json
from fastapi import FastAPI, Request, HTTPException
import uvicorn
app = FastAPI()
SHARED_KEY = b"supersecretkey"
def verify_signature(message_id, timestamp, payload, received_signature):
data = f"{message_id}.{timestamp}.{payload}".encode()
expected_signature = base64.b64encode(
hmac.new(SHARED_KEY, data, hashlib.sha256).digest()
).decode("utf-8")
return hmac.compare_digest(f"v1,{expected_signature}", received_signature)
@app.get("/")
async def health_check():
return {"status": "success", "message": "Webhook server is running"}
@app.post("/")
async def catch_all(request: Request):
headers = request.headers
message_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
received_signature = headers.get("webhook-signature")
payload_bytes = await request.body()
payload_json = payload_bytes.decode()
payload = await request.json()
if not all([message_id, timestamp, received_signature, payload]):
raise HTTPException(
status_code=400, detail="Missing required headers or payload"
)
if not verify_signature(message_id, timestamp, payload_json, received_signature):
raise HTTPException(status_code=403, detail="Invalid webhook signature")
return {"status": "success", "message": "Webhook received and verified"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8200)
Validating HTTPS certificates
When using webhooks over HTTPS, you can configure your system to validate certificates to ensure that the receiver has a valid HTTPS certificate, adding an extra layer of security.
Example payloads
Here are some example payloads of some events that may be sent.
- infrahub.branch.created
- infrahub.node.created
- infrahub.node.updated
{
"data": {
"branch_id": "182c6a84-4af4-736b-35a6-c514e640ca8b",
"branch_name": "test",
"sync_with_git": false
},
"id": "2ece726f-8574-488c-92f7-ee9a56485b46",
"branch": null,
"account_id": "182c6918-8880-5252-3611-c51ae7e0d6a5",
"occured_at": "2025-03-13 16:44:32.321142+00:00",
"event": "infrahub.branch.created"
}
{
"data": {
"kind": "OrganizationProvider",
"action": "created",
"fields": [
"name"
],
"node_id": "182c69ca-75cc-dff2-39ba-c51db89f91bd",
"changelog": {
"node_id": "182c69ca-75cc-dff2-39ba-c51db89f91bd",
"node_kind": "OrganizationProvider",
"attributes": {
"name": {
"kind": "Text",
"name": "name",
"value": "Google Fibre",
"properties": {
"is_protected": {
"name": "is_protected",
"value": false,
"value_type": "Boolean",
"value_previous": null,
"value_update_status": "added"
}
},
"value_previous": null,
"value_update_status": "added"
},
"description": {
"kind": "Text",
"name": "description",
"value": "Google Fibre",
"properties": {
"is_protected": {
"name": "is_protected",
"value": false,
"value_type": "Boolean",
"value_previous": null,
"value_update_status": "added"
}
},
"value_previous": null,
"value_update_status": "added"
}
},
"display_label": "Google Fibre",
"relationships": {}
}
},
"id": "bf569ddc-2a30-4491-82f1-0689f6cdb5e1",
"branch": "new-provider",
"account_id": "182c6918-8880-5252-3611-c51ae7e0d6a5",
"occured_at": "2025-03-13 16:31:14.566032+00:00",
"event": "infrahub.node.created"
}
{
"data": {
"kind": "InfraInterfaceL3",
"action": "updated",
"fields": [
"description",
"role"
],
"node_id": "182c697c-9915-aa00-39b7-c5181aeb3367",
"changelog": {
"node_id": "182c697c-9915-aa00-39b7-c5181aeb3367",
"node_kind": "InfraInterfaceL3",
"attributes": {
"role": {
"kind": "Dropdown",
"name": "role",
"value": "peering",
"properties": {},
"value_previous": "peer",
"value_update_status": "updated"
},
"description": {
"kind": "Text",
"name": "description",
"value": "Connected to IX",
"properties": {},
"value_previous": "Connected to atl1-edge2::Ethernet1",
"value_update_status": "updated"
}
},
"display_label": "Ethernet1",
"relationships": {}
}
},
"id": "67acc7ea-93b1-4175-a69b-801c869aea07",
"branch": "alter-interface",
"account_id": "182c6918-8880-5252-3611-c51ae7e0d6a5",
"occured_at": "2025-03-13 16:36:25.688038+00:00",
"event": "infrahub.node.updated"
}
Custom headers
Webhooks can include custom HTTP headers in every request they send. This is useful for authentication tokens, routing metadata, or any other headers the receiving system requires.
Key-value pair types
Custom headers are defined as reusable key-value pair entities that can be shared across multiple webhooks:
- Static Key-Value (
CoreStaticKeyValue): The header value is stored as plain text and sent as-is. Use this for non-sensitive metadata likeX-Source-System: infrahubor for authentication tokens likeAuthorization: Bearer <token>. - Environment Variable Key-Value (
CoreEnvironmentVariableKeyValue): The stored value is the name of an environment variable. The actual value is resolved from the Prefect worker's environment at the time the webhook request is sent. Use this when secrets are managed externally (for example, Kubernetes secrets or HashiCorp Vault).
Creating and associating headers
- Create a key-value pair via the UI or GraphQL API, specifying a name (human-friendly identifier), a key (the HTTP header field name), and a value.
- Associate the key-value pair with one or more webhooks using the headers relationship on the webhook.
A single key-value pair can be linked to multiple webhooks (many-to-many relationship). When the key-value pair is updated, all linked webhooks automatically pick up the new value on their next trigger.
Header merge behavior
When a webhook fires, headers are merged in this order:
- System defaults:
Accept: application/jsonandContent-Type: application/json - Custom headers: Applied on top of defaults — a custom header with the same key as a system default will override it
- HMAC signature headers (if a shared key is configured):
webhook-id,webhook-timestamp, andwebhook-signatureare added last and cannot be overridden by custom headers
Environment variable resolution
For environment-variable-based headers, the value is resolved from os.environ on the Prefect worker at send time:
- If the environment variable exists, its value is used as the header value.
- If the environment variable is not found, the header is skipped, a warning is logged, and all remaining headers are still sent.
Authentication header values must include the full value including any type prefix. For example, for Bearer token authentication, set the value to Bearer eyJhbGci... (not just the token).
Custom webhook transformation
The data object from the payloads above is passed to the transform method of your Python transform.