Author picture Phil Leggetter

Outpost v0.4.0 Released: Azure Service Bus Support + Full Feature Overview

Published · Updated


We're excited to announce the release of Outpost v0.4.0.

This release introduces support for Azure Service Bus queues and topics as an event destination. This makes Outpost even more flexible for developer tool companies building for teams on Microsoft infrastructure or Azure-based application stacks.

With this release, we're taking a moment to provide a complete overview of Outpost. In this post, we'll cover:

  • What's new in v0.4.0, including Azure Service Bus support
  • What Outpost is and how it works
  • Key Outpost features, including support for multiple event destination types
  • Why you might want to self-host your event delivery infrastructure

What's new in v0.4.0

Azure Service Bus event destination type support

You can now deliver events to Azure Service Bus queues and topics, with full support for Azure AD authentication using a service principal.

Azure Service Bus destination in Outpost UI

While you can create destinations (subscriptions) through the Outpost portal, you can also manage them programmatically using the Outpost SDKs (TypeScript, Go, and Python, generated with Speakeasy) and API.

Here's how you can use Outpost to create a tenant, define a destination, and publish an event to Azure Service Bus:

Create a tenant

import { Outpost } from "@hookdeck/outpost-sdk";

async function main() {
  const client = new Outpost({
    serverURL: `${process.env.SERVER_URL}/api/v1`,
    security: {
      adminApiKey: process.env.ADMIN_API_KEY,
    }
  });

  await client.tenants.upsert({
    tenantId: process.env.TENANT_ID,
  });
}

main();
package main

import (
  "context"
  outpostgo "github.com/hookdeck/outpost/sdks/outpost-go"
  "github.com/hookdeck/outpost/sdks/outpost-go/models/components"
  "log"
  "os"
)

func main() {
  adminAPIKey := os.Getenv("ADMIN_API_KEY")
  serverURL := os.Getenv("OUTPOST_URL")
  tenantID := os.Getenv("TENANT_ID")

  client := outpostgo.New(
    outpostgo.WithSecurity(components.Security{
      AdminAPIKey: &adminAPIKey,
    }),
    outpostgo.WithServerURL(fmt.Sprintf("%s/api/v1", serverURL)),
  )

  res, err := client.Tenants.Upsert(context.Background(), &tenantID)
}
import os
import sys
from outpost_sdk import Outpost, models


def run():
    sdk = Outpost(
        security=models.Security(admin_api_key=os.getenv("ADMIN_API_KEY")),
        server_url=f"{os.getenv("OUTPOST_URL", "http://localhost:3333")}/api/v1",
    )

    sdk.tenants.upsert(tenant_id=os.getenv("TENANT_ID", "hookdeck"))


if __name__ == "__main__":
    run()
curl -X POST https://<your-outpost-host>/api/v1/tenant \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "example-tenant"
  }'

Create an Azure Service Bus destination

import { Outpost } from "@hookdeck/outpost-sdk";
import { CreateTenantDestinationRequest } from '@hookdeck/outpost-sdk/dist/esm/models/operations';

async function main() {
  const outpostAdmin = new Outpost({
    security: { adminApiKey: process.env.ADMIN_API_KEY },
    serverURL: `${process.env.SERVER_URL}/api/v1`,
  });

  const destinationCreateRequest: CreateTenantDestinationRequest = {
      tenantId: process.env.TENANT_ID,
      destinationCreate: {
        credentials: {
          connectionString,
        },
        type: "azure_servicebus",
        config: {
          name: process.env.TOPIC_OR_QUEUE_NAME,
        },
        topics: "*",
      }
    };

  const destination = await outpostAdmin.destinations.create(destinationCreateRequest);
  console.log("Destination created successfully:", destination);
}

main();
package main

import (
  "context"
  "fmt"
  "log"
  "os"

  outpostgo "github.com/hookdeck/outpost/sdks/outpost-go"
  "github.com/hookdeck/outpost/sdks/outpost-go/models/components"
)

func main() {
  adminAPIKey := os.Getenv("ADMIN_API_KEY")
  serverURL := os.Getenv("OUTPOST_URL")
  tenantID := os.Getenv("TENANT_ID")

  client := outpostgo.New(
    outpostgo.WithSecurity(components.Security{
      AdminAPIKey: &adminAPIKey,
    }),
    outpostgo.WithServerURL(fmt.Sprintf("%s/api/v1", serverURL)),
  )

  topics := components.CreateTopicsTopicsEnum(components.TopicsEnumWildcard)

  destination := components.CreateDestinationCreateAzureServicebus(
    components.DestinationCreateAzureServiceBus{
      Topics: topics,
      Config: components.AzureServiceBusConfig{
        Name: topicOrQueueName,
      },
      Credentials: components.AzureServiceBusCredentials{
        ConnectionString: connectionString,
      },
    },
  )

  res, err := client.Destinations.Create(context.Background(), destination, &tenantID)
  if err != nil {
    log.Fatalf("failed to create destination: %v", err)
  }

  if res.Destination != nil && res.Destination.ID != nil {
    log.Printf("Successfully created destination with ID: %s", *res.Destination.ID)
  }
}
import os
import sys
import questionary
from dotenv import load_dotenv
from outpost_sdk import Outpost, models


def run():
    sdk = Outpost(
        security=models.Security(admin_api_key=os.getenv("ADMIN_API_KEY")),
        server_url=f"{os.getenv("OUTPOST_URL", "http://localhost:3333")}/api/v1",
    )

    resp = sdk.destinations.create(
        tenant_id=os.getenv("TENANT_ID", "hookdeck"),
        destination_create=models.DestinationCreateAzureServiceBus(
            type=(models.DestinationCreateAzureServiceBusType.AZURE_SERVICEBUS),
            credentials=models.AzureServiceBusCredentials(
                connection_string=os.getenv("AZURE_SERVICE_BUS_CONNECTION_STRING", "your_connection_string")
            ),
            config=models.AzureServiceBusConfig(name=os.getenv("TOPIC_OR_QUEUE_NAME", "your_topic_or_queue_name")),
            topics="*",
        ),
    )

    if resp:
        print("Destination created successfully:", resp)
    else:
        print("Failed to create destination, response was empty.")


if __name__ == "__main__":
    run()
curl -X POST 'http://localhost:3333/api/v1/example-tenant/destinations' \
--header 'Authorization: Bearer <API_KEY>' \
--header 'Content-Type: application/json' \
--data-raw '{
    "type": "azure_servicebus",
    "topics": ["order.created", "order.updated", "order.cancelled"],
    "config": {
        "name": "my-azure-queue"
    },
    "credentials": {
        "connection_string": "Endpoint=sb://your-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=your-shared-access-key"
    }
}'

Publish an event

import { Outpost } from "@hookdeck/outpost-sdk";
import dotenv from "dotenv";
dotenv.config();


async function main() {
  const client = new Outpost({
    serverURL: `${SERVER_URL}/api/v1`,
    security: {
      adminApiKey: API_KEY,
    }
  });

  const topic = "order.created";
  const payload = {
    order_id: "ord_2Ua9d1o2b3c4d5e6f7g8h9i0j",
    customer_id: "cus_1a2b3c4d5e6f7g8h9i0j",
    total_amount: "99.99",
    currency: "USD",
    items: [
        {
            product_id: "prod_1a2b3c4d5e6f7g8h9i0j",
            name: "Example Product 1",
            quantity: 1,
            price: "49.99",
        },
    ],
  };

  try {
    const response = await client.publish.event({
      topic: topic,
      data: payload,
      tenantId: tenantId,
    });
    console.log("Event published successfully:", response);
  } catch (error) {
    console.error("Error publishing event:", error);
  }
}

main();
package main

import (
  "context"
  "encoding/json"
  "fmt"
  "log"
  "os"

  outpostgo "github.com/hookdeck/outpost/sdks/outpost-go"
  "github.com/hookdeck/outpost/sdks/outpost-go/models/components"
)

func runPublishEventExample() {
  adminAPIKey := os.Getenv("ADMIN_API_KEY")
  serverURL := os.Getenv("OUTPOST_URL")
  tenantID := os.Getenv("TENANT_ID")

  client := outpostgo.New(
    outpostgo.WithSecurity(components.Security{
      AdminAPIKey: &adminAPIKey,
    }),
    outpostgo.WithServerURL(fmt.Sprintf("%s/api/v1", serverURL)),
  )

  topic := "order.created"
  payload := map[string]interface{}{
    "order_id":     "ord_2Ua9d1o2b3c4d5e6f7g8h9i0j",
    "customer_id":  "cus_1a2b3c4d5e6f7g8h9i0j",
    "total_amount": "99.99",
    "currency":     "USD",
    "items": []map[string]interface{}{
      {
        "product_id": "prod_1a2b3c4d5e6f7g8h9i0j",
        "name":       "Example Product 1",
        "quantity":   1,
        "price":      "49.99",
      },
    },
  }

  payloadBytes, err := json.Marshal(payload)
  if err != nil {
    log.Fatalf("Failed to marshal payload: %v", err)
  }

  var data map[string]interface{}
  err = json.Unmarshal(payloadBytes, &data)
  if err != nil {
    log.Fatalf("Failed to unmarshal payload into data: %v", err)
  }

  request := components.PublishRequest{
    Topic:    &topic,
    Data:     data,
    TenantID: &tenantID,
  }

  res, err := client.Publish.Event(context.Background(), request)
  if err != nil {
    log.Fatalf("Error publishing event: %v", err)
  }

  if res.HTTPMeta.Response.StatusCode == 202 {
    fmt.Println("Event published successfully")
    if res.PublishResponse != nil {
      fmt.Printf("Event ID: %s\n", res.PublishResponse.GetID())
    }
  } else {
    fmt.Printf("Failed to publish event. Status code: %d\n", res.HTTPMeta.Response.StatusCode)
  }
}

import os
from outpost_sdk import Outpost, models


def run():

    sdk = Outpost(
        security=models.Security(admin_api_key=os.getenv("ADMIN_API_KEY")),
        server_url=f"{os.getenv("OUTPOST_URL", "http://localhost:3333")}/api/v1",
    )

    topic = "order.created"
    payload = {
        "order_id": "ord_2Ua9d1o2b3c4d5e6f7g8h9i0j",
        "customer_id": "cus_1a2b3c4d5e6f7g8h9i0j",
        "total_amount": "99.99",
        "currency": "USD",
        "items": [
            {
                "product_id": "prod_1a2b3c4d5e6f7g8h9i0j",
                "name": "Example Product 1",
                "quantity": 1,
                "price": "49.99",
            },
        ],
    }

    res = sdk.publish.event(
        topic=topic,
        data=payload,
        tenant_id=tenant_id,
    )

    if res is not None:
        print("Event published successfully")
        print(f"Event ID: {res.id}")
    else:
        print("Failed to publish event")

curl -X POST https://<your-outpost-host>/api/v1/publish \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "topic": "order.created",
    "tenant_id": "example-tenant",
    "event": {
      "data": {
        "order_id": "ORD-8721",
        "total": 149.99,
        "currency": "USD",
        "items": [
          {"sku": "sku-123", "quantity": 2},
          {"sku": "sku-456", "quantity": 1}
        ],
        "customer": {
          "id": "cust-001",
          "email": "jane.doe@example.com"
        }
      }
    }
  }'

Once published, Outpost handles the delivery to the configured Azure Service Bus destination.

Check out the TypeScript, Go, and Python examples directories for more details.

Other improvements and fixes

  • In addition to using the API to publish events, you can configure Outpost to read messages from a "publish" queue. v0.4.0 adds support for Azure Service Bus as a publish message queue.
  • When Outpost services (api, log, and delivery) start, they provision the necessary message queue infrastructure. This release includes a locking mechanism to ensure that only one service instance handles the setup process at a time.

What is Outpost?

Outpost is a self-hosted event delivery service. You send events to Outpost via an API or supported message queue, and it reliably delivers them to your configured destinations.

Outpost architecture diagram

Unlike the Hookdeck Event Gateway, Outpost doesn't handle event ingestion from API providers. Instead, it's designed to solve the other side of the problem: controlling and observing how your system delivers outbound events to multiple event destination types.

How Outpost works

How Outpost works

  1. Your system pushes an event to Outpost via the API or a supported publish queue.
  2. Outpost routes the event based on its topic and (optionally) the tenant.
  3. It attempts to deliver the event to the configured destination (e.g., webhook endpoint, message queue).
  4. If a delivery fails, Outpost automatically retries with an exponential backoff strategy.
  5. Outpost logs every attempt, allowing developers to inspect and replay events as needed.

Outpost features

  • Multi-Tenant Support: Run multiple isolated tenants on a single deployment
  • Event Topics & Routing: Route events by topic/event type to appropriate destinations
  • At-Least-Once Delivery: Guaranteed message delivery with no data loss
  • Event Fanout: Replicate messages to multiple endpoints for parallel processing
  • Automatic & Manual Retries: Configurable retry strategies with manual retry options
  • Delivery Failure Alerts: Get notified when deliveries fail repeatedly
  • Multiple Ingestion Methods: Publish via API or supported message queues
  • Webhook Best Practices: Built-in support for idempotency, signatures, and timestamps
  • Multiple Destination Support: HTTP webhooks, Hookdeck Event Gateway, message queues (RabbitMQ, AWS SQS, Azure Service Bus), and streaming services (AWS Kinesis)
  • Tenant User Portal: Allow customers to manage their destinations and inspect events
  • Observability: OpenTelemetry integration for traces, metrics, and logs
  • Configurable Logging: Control log verbosity across services

Full list: Outpost features

Why self-host outbound delivery?

Self-hosting your event delivery infrastructure offers several key advantages for companies building developer tools, API platforms, or internal event systems:

Control and compliance

  • Maintain full control over where your data is stored and processed
  • Meet data residency requirements by hosting in specific regions
  • Integrate with existing internal security and compliance systems
  • Deploy behind your firewall for sensitive workloads

Performance and reliability

  • Deploy closer to your core systems to minimize latency
  • Scale independently based on your specific traffic patterns
  • Avoid external dependencies for business-critical event flows
  • Configure custom retry policies tailored to your use cases

Cost efficiency at scale

  • Eliminate per-event fees that become expensive at high volumes
  • Leverage existing infrastructure investments
  • Control resource allocation based on your priorities

Common use cases

Outpost is particularly valuable when you're:

  • Building a developer platform with webhooks as a core feature
  • Modernizing architectures by connecting legacy systems to cloud services
  • Operating multi-tenant SaaS with customer-specific delivery requirements
  • Managing high-volume event streams that need reliable delivery guarantees
  • Implementing event-driven architectures where delivery observability matters

For these scenarios, Outpost provides the benefits of a managed event delivery service while giving you the control of a self-hosted solution.

Try Outpost v0.4.0

Ready to get started with Outpost?

We welcome contributions, feedback, and feature requests. Let us know what destinations or features you'd like to see next.