Fused

Not every integration goes outward

Some integrations go the other way: you need to know when a long-running job finishes or something changes in a service you use. Fused gives your Engine its own webhook URL, checks signatures for you and sends verified events to your apps, SDKs and agents, without you exposing a server.

Most of what we write about integrations goes in one direction. Your code calls Stripe, your agent updates a Jira issue, your schedule sends a report. You ask, the provider answers.

A lot of real work goes the other way. A payment takes days to settle. A CI run finishes twenty minutes after you started it. A customer files an issue, a teammate pushes a fix, a user finishes connecting their Google account in another tab. You didn't ask about any of these at that moment, but your product needs to know about them so it can react.

Providers tell you through webhooks. Receiving them is where the work piles up: a public endpoint for each provider, a signature check written slightly differently every time, retries, duplicates, and an event that reaches only the one process that happened to receive it. If the thing that should react is a local tool, an internal service or an agent, you also have to open a door to the internet that you'd rather keep shut.

Fused handles that inbound side the same way it handles outbound calls. Events arrive at your Engine, Fused verifies them, and you decide which parts of your system get them.

flowchart TD
  accTitle: How a provider event moves through Fused
  accDescr: A provider sends a webhook to your Engine's URL. Engine verifies the signature, then delivers the event to a Unified App, an SDK receiver or an MCP event resource.
  P[Provider sends webhook] --> E[Your Engine verifies it]
  E --> A[Unified App]
  E --> S[Your SDK]
  E --> M[Your agent]

Get a URL that belongs to your Engine

You describe which services you want events from, apply it, and Fused gives you a URL for each one.

1. Config

List the services and where each one's signing secret lives:

apiVersion: fused/v1
kind: webhook
name: team-events
services:
  jira:
    secret: "${bucket.default.secret.jira_signing}"
  github:
    secret: "${bucket.default.secret.github_signing}"

2. Apply

fused-cli webhook plan
fused-cli webhook apply

Apply prints a URL for each registration. Paste it into the provider's webhook settings, the same way you would for any other receiver.

3. Check

fused-cli webhook list --service jira

This shows what's registered and whether a signature is set. It never prints the secret itself.

The difference from any other receiver is that the URL points at your Engine. If you self-host Engine, that means your own infrastructure, not a third-party relay.

The signing secrets are bucket references, so they sit next to your other credentials and never appear in the config file. The secret has to be in the bucket before the first event arrives.

Fused checks the signature for you

Every provider signs its webhooks in its own way, and every team has to write the check again. Get it wrong and anyone who finds the URL can send you fake events.

Engine checks the signature against the secret in your bucket before it lets the event in. An event that fails the check doesn't reach your code at all. A handler can trust that an event it receives came from the provider, without carrying a copy of the signing secret or a verification library for each service.

Decide what reacts

A verified event is only useful once something acts on it. Fused gives you three ways to do that, and you can use more than one for the same event.

Unified App

If the reaction is a fixed process, such as triaging a new issue, syncing a record or notifying a channel, let the event start a Unified App. Attach the webhook config and choose the events:

webhook_attachment: team-events
services:
  jira:
    version: "<enabled-service-version>"
    operations: [getIssue]
    webhooks: [issue.created]

Once it's applied, Engine usually starts listening within five seconds. The event becomes the app's input. If the provider sends the same event twice, the app still runs once. You don't run a service for any of this.

Your SDK

Sometimes the reaction belongs in your own code, like updating a live dashboard, waking a background worker or refreshing a cache. Add the events to your generated SDK. One SDK can collect events from several services, alongside the operations it already calls:

apiVersion: fused/v1
kind: sdk
name: support
version: "1.2.0"
bucket: default
webhook_attachment: team-events
services:
  jira:
    operations: [createIssue]
    webhooks: ["issue.created", "issue.updated"]
  github:
    operations: [listRepos]
    webhooks: ["push"]

The generated SDK ships a receiver with typed constants for each event. The next section shows the handler code.

Your agent

Agents usually only act when someone asks them to. With event resources, an agent can respond to things as they happen.

An MCP server can expose provider events as resources next to its tools. You choose the events the same way:

apiVersion: fused/v1
kind: mcp
name: slack-events
version: "1.0.0"
description: Inspect mentions received by the connected Slack app.
bucket: connections
webhook_attachment: managed-slack-events
services:
  slack:
    version: "v1"
    webhooks: ["app_mention"]

The client listens on the event's resource URI, which looks like fused://events/<service-id>/app_mention. When a mention arrives, the client gets a notification that names the resource. It reads the resource to get the latest event, and decides what the agent should do. The agent can use the same server's search_docs and execute tools to act on the event, with the same scopes you set for everything else.

One thing to plan for: MCP notifications aren't a queue. If the client is disconnected when an event arrives, it gets the latest event when it reconnects and reads again, not every event it missed. If every occurrence has to be handled, add an SDK receiver for the same events and use the agent for the parts that need judgement.

Your code doesn't need a public address

The SDK receiver works the other way round from a normal webhook endpoint. Your process opens an outbound connection to Fused, and events come back over it. It never accepts an inbound request from Jira or GitHub.

TypeScript

import { FusedWebhooks, JiraWebhook } from 'support-sdk';

const receiver = FusedWebhooks.listen('issue-triage', process.env.FUSED_APP_TOKEN!);

receiver.on(JiraWebhook.ISSUE_CREATED, async (payload, ctx) => {
  try {
    await triage(payload);
    ctx.ack();
  } catch {
    ctx.nack();
  }
});

receiver.on(JiraWebhook.ISSUE_UPDATED, handleChange);

Python

import os

from fused.support_sdk import FusedWebhooks
from fused.support_sdk.webhooks import JiraWebhook

receiver = FusedWebhooks.listen("issue-triage", os.environ["FUSED_APP_TOKEN"])

async def on_created(payload, ctx):
    try:
        await triage(payload)
    except Exception:
        await ctx["nack"]()
        return
    await ctx["ack"]()

receiver.on(JiraWebhook.ISSUE_CREATED, on_created)
receiver.on(JiraWebhook.ISSUE_UPDATED, handle_change)

That means a tool running on a laptop, a service inside a private network, or an internal app with no public hostname can all react to provider events in real time. Nothing needs to be exposed to a third party: no tunnel, no public load balancer, no firewall exception.

Delivery is durable. If a handler calls nack(), the event is delivered again. If your process is down, pending events wait until it reconnects. Delivery is at least once, so write handlers that are safe to run twice.

The first argument to listen() names the consumer, not the SDK. Every instance that listens with the same name shares one stream of events, so if you scale issue-triage to three replicas, each event goes to one of them, not all three. Another service using the same SDK listens with its own name, say live-dashboard, and gets its own copy of every event.

Some events come from Fused itself

Not every "tell me when it's done" comes from a provider. When one of your users connects an OAuth account, the connection finishes in their browser and your code has no direct way to know.

If an SDK uses connected OAuth, it receives Fused's own auth lifecycle events with no webhook config at all: connection completed, token refreshed, refresh failed, reconnect required. You handle them like any other event:

TypeScript

receiver.on(JiraWebhook.FUSED_AUTH_CONNECTION_COMPLETED, async (event) => {
  await handleConnectionComplete(event.end_user_ref);
});
receiver.on(JiraWebhook.FUSED_AUTH_CONNECTION_RECONNECT_REQUIRED, handleReconnect);

Python

receiver.on(JiraWebhook.FUSED_AUTH_CONNECTION_COMPLETED, handle_connection_complete)
receiver.on(JiraWebhook.FUSED_AUTH_CONNECTION_RECONNECT_REQUIRED, handle_reconnect)

You can then continue an onboarding flow when the user is ready, or ask them to reconnect before a scheduled job fails, without polling for either.

Before you rely on it

Try it

  1. Install the CLI and sign in.
  2. Write a webhook config for one service you already use, run fused-cli webhook apply, and paste the URL into that provider.
  3. Send its events to your SDK, to your agent, or to a Unified App.