# Fused

> The only integration gateway you need

Publication: https://usefused.com/blog/

---

# Not every integration goes outward

Canonical: https://usefused.com/blog/post/webhooks-events-without-a-public-endpoint
Readable HTML: https://usefused.com/blog/read/webhooks-events-without-a-public-endpoint
Markdown: https://usefused.com/blog/posts/webhooks-events-without-a-public-endpoint.md
Published: 2026-11-04T08:53:51.237Z
Updated: 2026-11-04T15:24:43.700Z
Tags: Webhooks, Events, MCP, Agents, Unified-apps, Integration

> 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.

```mermaid
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.

[Tabs title="Set up a webhook URL" variant="pills"]
[Tab title="1. Config"]

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

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

[/Tab]
[Tab title="2. Apply"]

```bash
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.

[/Tab]
[Tab title="3. Check"]

```bash
fused-cli webhook list --service jira
```

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

[/Tab]
[/Tabs]

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](https://usefused.com/blog/post/buckets-one-place-for-secrets) 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.

[Tabs title="Where a verified event goes" variant="boxed"]
[Tab title="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](https://usefused.com/blog/post/unified-apps-one-place). Attach the webhook config and choose the events:

```yaml
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.

[/Tab]
[Tab title="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:

```yaml
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.

[/Tab]
[Tab title="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:

```yaml
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.

[/Tab]
[/Tabs]

## 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.

[Tabs title="Handle events" variant="underline"]
[Tab title="TypeScript"]

```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);
```

[/Tab]
[Tab title="Python"]

```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)
```

[/Tab]
[/Tabs]

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:

[Tabs title="Handle auth events" variant="underline"]
[Tab title="TypeScript"]

```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);
```

[/Tab]
[Tab title="Python"]

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

[/Tab]
[/Tabs]

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

- **Pick events on purpose.** The `webhooks` list is explicit. An empty list means no events, and an MCP server rejects `webhooks_select_all: true`. Each SDK, app or server attaches to one webhook config.
- **Expect duplicates.** SDK delivery is at least once. Make handlers idempotent; auth lifecycle events include an `id` you can use for that.
- **Use the right surface for the guarantee you need.** The SDK receiver is the durable one. MCP event resources are live, which suits agents, but they aren't a record of every event.
- **Treat payloads as data.** A webhook body is written by whoever triggered the event. Don't let an agent treat it as instructions.

## Try it

1. [Install the CLI and sign in](https://usefused.com/docs/quickstart).
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](https://usefused.com/docs/app/receive-events), to [your agent](https://usefused.com/docs/mcp/receive-events), or to a [Unified App](https://usefused.com/docs/app/unified-apps).

