GitHub
Concept

Webhooks

Outbound webhooks sign events with HMAC-SHA256 and ship them to your URL; inbound webhooks accept signed payloads and emit platform events that trigger rules and workflows can react to.

Webhooks are how ProxifAI connects to systems that aren’t covered by a first-party integration. Outbound webhooks ship platform events to any HTTPS endpoint with HMAC-signed bodies. Inbound webhooks accept signed payloads from external systems and turn them into webhook.* platform events that trigger rules and workflow triggers: blocks can react to.

Outbound webhooks

When a platform event fires (issue created, PR merged, agent finished, …), every subscribed outbound webhook gets a POST request with the event as the JSON body. Implementation: internal/integrations/events.go.

Request shape

POST https://your-app.example.com/proxifai-webhook HTTP/1.1
Content-Type: application/json
X-ProxifAI-Event: issue.created
X-ProxifAI-Delivery: del_01H2T4...
X-ProxifAI-Signature: sha256=<hex>

{
  "id": "evt_…",
  "type": "issue.created",
  "source": "proxifai",
  "subject": "issue/abc123",
  "orgId": "org_…",
  "timestamp": "2026-05-05T13:14:15Z",
  "data": {
    "issueId": "abc123",
    "title": "Fix login bug",
    "status": "todo",
    "priority": "high",
    "teamId": "team1",
    "assigneeId": "user1"
  }
}

The body is the PlatformEvent JSON (internal/integrations/types.go) — same shape that trigger-rule conditions evaluate against. The data payload follows the JSON-Schema declared by the event type (see the built-in event catalog).

Verifying signatures

X-ProxifAI-Signature is sha256=<hex> where <hex> is HMAC-SHA256(body, secret) (events.go:287).

# Bash
EXPECTED="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')"
[ "$EXPECTED" = "$RECEIVED_SIGNATURE" ] && echo OK
# Python
import hmac, hashlib
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
hmac.compare_digest(expected, received_signature)
// Go
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
hmac.Equal([]byte(expected), []byte(receivedSignature))

Always use a constant-time compare. Reject requests where the header is missing or the comparison fails.

Creating an outbound webhook

pfai outbound-webhook create \
  --url https://your-app.example.com/proxifai-webhook \
  --events issue.created,issue.status_changed,pr.merged \
  --secret "$(openssl rand -hex 32)"

Or via the API:

curl -X POST http://localhost:3000/api/v1/outbound-webhooks \
  -H "Authorization: Bearer $PFAI_TOKEN" \
  -d '{
    "url": "https://your-app.example.com/proxifai-webhook",
    "events": ["issue.created","pr.merged"],
    "secret": "..."
  }'

The secret is stored encrypted at rest. Subscribe to ["*"] to receive every event type.

Delivery, retries, and tracking

Each delivery writes a row to WebhookDelivery with status (pending / success / failed). The retry policy is intentionally minimal in OSS — failures increment the webhook’s failureCount field but the delivery worker doesn’t auto-retry beyond the initial attempt. To rebuild a more aggressive retry behavior, run a workflow that listens for webhook.failed (a synthetic event the delivery worker emits on 5xx) and re-fires.

Tracked per webhook:

FieldSource
sendCount, failureCount, lastSentAtUpdated on every delivery attempt
Per-delivery row in WebhookDeliverydelivery_id (X-ProxifAI-Delivery), event type, status, latency, response code

Inspect via pfai outbound-webhook ls/view <id> or GET /api/v1/outbound-webhooks/{id}/deliveries.

Event coverage

Every entry in the built-in event catalog is deliverable. Use ["*"] to subscribe to all 17, or pick specific types. Custom event types created via pfai event-type create are also deliverable.

Inbound webhooks

External systems POST to ProxifAI; ProxifAI authenticates the request, normalizes it into a PlatformEvent, and publishes it on the event bus where trigger rules and workflows can react.

URL format

POST /api/v1/webhooks/inbound/<slug>

The slug is generated when you create an integration — it’s the secret. Anyone with the URL can deliver events to your org, so treat it like a credential.

curl -X POST http://localhost:3000/api/v1/webhooks/inbound/wh_a1b2c3... \
  -H "Content-Type: application/json" \
  -H "X-Event-Type: alert.fired" \
  -d '{"severity":"critical","message":"DB latency > 500ms"}'

Authentication

Two layered defenses:

  1. The slug is unguessable — generated as a random token, scoped to a single integration row.
  2. HMAC signature (optional but recommended) — provide a secret on the integration; ProxifAI verifies the body against X-Hub-Signature-256 (GitHub-style) or X-Webhook-Signature (internal/integrations/generic/generic.go:36). Format is sha256=<hex>.

Without a configured secret, the slug is the only authentication. Always set a secret for production endpoints.

Provider routing

Inbound webhooks are dispatched to the provider implementation matching the integration’s provider field:

providerUse caseBehavior
generic_webhookCatch-all for systems without a dedicated providerWraps the body into a webhook.received event with the X-Event-Type header (or X-GitHub-Event if present) as eventType in the data
slackSlack Events API + slash commandsVerifies Slack’s signing secret, parses event, dispatches to commands/messages
mcp_serverStubThe MCP integration provider exists for symmetry but rejects inbound webhooks (the MCP server lives at /api/v1/mcp/*, not here)

Custom providers can be registered via integrations.RegisterProvider to handle vendor-specific signing schemes.

Reacting to inbound events

Once the event is published, two systems can act on it:

  • Trigger rulesevent_source + event_type filter + ConditionGroup → action (dispatch_agent, create_issue, send_webhook, dispatch_workflow)
  • Workflow YAML triggerstriggers: [{type: event, event: webhook.received}] blocks fire workflows directly

Both have full access to the event’s data payload via the standard JMESPath-style field references.

REST endpoints

Method · PathPurpose
GET /api/v1/outbound-webhooksList outbound webhooks
POST /api/v1/outbound-webhooksCreate — {url, events[], secret}
PATCH /api/v1/outbound-webhooks/{id}Update target URL, event subscriptions, or secret
DELETE /api/v1/outbound-webhooks/{id}Delete
GET /api/v1/outbound-webhooks/{id}/deliveriesPer-delivery audit log
POST /api/v1/integrationsCreate an integration (which provisions the inbound URL)
POST /api/v1/webhooks/inbound/{slug}Deliver an inbound event

pfai outbound-webhook and pfai integration wrap these.

See also