This guide will help you migrate your existing webhook infrastructure to Outpost. It assumes you are migrating from a notification infrastructure that only supports webhook event destination types.

Why Migrate to Outpost?

Outpost is available as both a managed Hookdeck service and a self-hosted open-source deployment. It enables event producers to add Event Destinations to their platform with support for destination types such as Webhooks, Hookdeck Event Gateway, AWS SQS, AWS S3, GCP Pub/Sub, RabbitMQ, and Kafka.

The benefits of migrating to Outpost include:

For Event Producers:

  • Efficiency gains

    Reduced failure rates and reliable redelivery compared to public HTTP endpoints. Unlock improved performance for high-throughput scenarios.

  • Protocol flexibility

    Leverage more performant protocols and encodings.

  • Cost & resource efficiency

    Smart retry logic, improved deliverability, and scalable infrastructure minimize resource consumption, reducing operational costs while ensuring seamless event delivery at any scale.

For Event Consumers:

  • Streamlined infrastructure and operations

    Eliminate the need for API gateways, load balancers, HTTP consumers, and other infrastructure components, reducing maintenance overhead.

  • Reduced developer burden

    Receive events directly to existing or preferred infrastructure and use existing and familiar ecosystem tooling.

  • Predictable behavior

    Standardize event expectations—the message bus handles timeouts, retries, and security.

How to Migrate to Outpost

The following sections outline the key considerations when migrating webhook infrastructure to Outpost.

Terminology

Outpost has the following concepts that you need to map to your existing webhook infrastructure provider:

  • Tenants: A tenant represents a customer organization.
  • Destination Types: The type of destination where events will be delivered. For example, webhook, Hookdeck Event Gateway, AWS SQS, or AWS S3.
  • Destinations: A subscription configured to deliver the event to a destination type. For example, a webhook destination with a specific URL.
  • Events: An event is a piece of data that represents an action that occurred in your system. For example, a user signed up or a payment was processed.
  • Topics: A topic is a way to categorize events and is a common concept found in Pub/Sub messaging. For example, a user.created.

Webhook Event Delivery Method

Webhooks are delivered via HTTP POST requests.

Webhook Event HTTP Headers

  • x-outpost-event-id: The event id (same as publish id when you provide one). Use it as the deduplication / idempotency key for at-least-once delivery; the x-outpost- prefix is configurable.
  • x-outpost-signature: HMAC of the raw body (default v0=<hex>; operators may customize templates — see Webhook destination).
  • x-outpost-timestamp: A Unix timestamp representing the time the event was generated
  • x-outpost-topic: The topic of the event. For example, user.created.

The x-outpost prefix can be customized. See Webhook Customization.

Webhook Event Payload Format

Outpost events are JSON payloads.

Webhook Event Payload Structure

Outpost does not enforce a specific event payload structure. You can define the structure of the event payload as needed. However, the content must be JSON.

Authentication

By default, Outpost uses a secret key to sign the webhook payload. The secret key is used to generate a signature that is included in the x-outpost-signature header. The signature is generated using the HMAC-SHA256 algorithm with Hex encoding.

The x-outpost prefix can be customized. See Webhook Customization.

Webhook Customization

You can customize the following webhook features and behaviors. The full path to the YAML configuration is prefixed with destinations.webhook.

Templates use the Go template syntax.

In managed Outpost, prefer the Config API for migration automation and anything you want to drive from scripts, pipelines, or infrastructure-as-code. The same values are editable in the Hookdeck dashboard under your Outpost project when you want to review or change settings by hand.

Topics and retry defaults

Use the Config API with TOPICS, MAX_RETRY_LIMIT, RETRY_INTERVAL_SECONDS, and RETRY_SCHEDULE so topic and retry defaults stay alongside the rest of your automated migration. The same fields are available in Hookdeck General settings.

For more on how topics and delivery fit together, see Concepts and Event Delivery & Retries.

Webhook headers and signatures

Use the Config API with the DESTINATIONS_WEBHOOK_* keys to automate webhook headers, signatures, and related destination behavior (the managed counterpart to self-hosted webhook environment variables). The same options can be changed in Hookdeck Destinations settings.

See Webhook destination for templates, defaults, and the full key list.

Portal and tenant-facing options

Use the Config API with PORTAL_* keys for the refresh URL, branding, and other tenant-facing portal options. The same settings are available in Hookdeck User Portal settings.

See Tenant Portal for how those options affect operators and end users.

Tenants, destinations, and backfill

Use the Outpost API or an SDK to create or upsert tenants, create destinations (including topics and secrets), and publish or backfill historical events in batches—this is the path that scales for migration automation. Config keys and dashboard settings above define platform defaults; this step is about moving organizations and subscriptions into Outpost.

You can also perform many tenant and destination actions from the Hookdeck dashboard when you want a manual or exploratory workflow alongside scripted migration.

For walkthroughs, see the curl, TypeScript, Python, and Go quickstarts, the API reference, and the SDKs overview.

YAMLEnvironment VariableDefault Value
header_prefixDESTINATIONS_WEBHOOK_HEADER_PREFIXx-outpost
disable_default_event_id_headerDESTINATIONS_WEBHOOK_DISABLE_DEFAULT_EVENT_ID_HEADERfalse
disable_default_signature_headerDESTINATIONS_WEBHOOK_DISABLE_DEFAULT_SIGNATURE_HEADERfalse
disable_default_timestamp_headerDESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TIMESTAMP_HEADERfalse
disable_default_topic_headerDESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TOPIC_HEADERfalse
signature_content_templateDESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE{{.Body}}
signature_header_templateDESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATEv0={{.Signatures | join ","}}
signature_encodingDESTINATIONS_WEBHOOK_SIGNATURE_ENCODINGhex
signature_algorithmDESTINATIONS_WEBHOOK_SIGNATURE_ALGORITHMhmac-sha256

Migration Process

It's recommended to have a test organization within Outpost, to test the migration process. This will allow you to test the migration process without delivering events to a customer.

First, perform a test migration:

  1. Perform a lookup of all the topics that are being used in your webhook infrastructure. Define them within your Outpost configuration.

  2. For each organization within your webhook infrastructure, create a tenant in Outpost.

  3. Each webhook subscription should map to a destination in Outpost. If webhook subscriptions include topic or event type information, then that should be included within the destination resource that is created. Also migrate the webhook secret.

  4. Migrate your historical event data to Outpost.

  5. Test the migration with your test organization. Ensure events are being delivered to the correct destinations. Check the payload and header contents, and verify that the webhook signature format can be validated.

For the full migration, you may need to:

  1. Notify your customers of scheduled maintenance ahead of time, and again notify them when the maintenance begins and ends.
  2. Prevent new webhook subscriptions from being created.
  3. Ensure all in-flight events are completed (either delivered or failed) before the migration begins.
  4. Queue any new events for publishing. This works well if you are using a publish message queue with Outpost. If you are using the Outpost API to publish events, you will need to store the events to be published once the migration has completed.
  5. Follow the steps 1 - 4 in the test migration process.
  6. Resume delivering the queued events via Outpost.

Migrating Historical Event Data

Managed Outpost does not provide direct database access. For historical event migration, backfill by publishing historical events through the Outpost API or an SDK in controlled batches, preserving any external identifiers in metadata so downstream systems can deduplicate or correlate.

Recommended managed approach:

  1. Export historical events from your existing system.
  2. Re-publish them to Outpost by tenant/topic in batches.
  3. Verify deliveries and retries in Hookdeck logs and metrics.
  4. Gradually increase batch size and then switch live traffic.

To migrate your historical data to Outpost, you map your existing structure into Outpost’s logical model (tenants, destinations, published events). For self-hosted installs you can also inspect the PostgreSQL schema that stores the event log; the canonical definitions are the migration files under internal/migrator/migrations/postgres in the Outpost repository.

Outpost persists published events and delivery history in two time-partitioned tables:

events — one row per distinct published event (retries reuse the same row). Columns include id, tenant_id, time, topic, eligible_for_retry, matched_destination_ids (text array of destination IDs that matched the event), data (payload stored as text so JSON key order is preserved), and metadata (JSON). There is no destination_id column on events; routing to destinations is expressed through matched_destination_ids and the delivery pipeline.

attempts — one row per delivery attempt to a destination. Columns include id, event_id, tenant_id, destination_id, destination_type, topic, status, time, attempt_number, manual, code, response_data (HTTP-style response body stored as text), plus denormalized event fields (event_time, eligible_for_retry, event_data, event_metadata) for efficient querying.

Writing rows directly is possible for advanced operators, but easy to get wrong (partitions, denormalized attempt fields, and invariants enforced in application code). For historical replay, prefer publishing through the Outpost API or an SDK unless you are deliberately doing a low-level database migration.

Example Migration Script

The following uses the current @hookdeck/outpost-sdk (Outpost client, serverURL + apiKey) to mirror tenants and webhook destinations from a placeholder db module—same pattern as the runnable demo.

import process from "node:process";
import dotenv from "dotenv";
dotenv.config();

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

// Replace with your own source of organizations and subscriptions.
import { default as db } from "./db";

if (!process.env.OUTPOST_API_BASE_URL || !process.env.OUTPOST_API_KEY) {
  console.error("OUTPOST_API_BASE_URL and OUTPOST_API_KEY are required");
  process.exit(1);
}

const outpost = new Outpost({
  serverURL: process.env.OUTPOST_API_BASE_URL,
  apiKey: process.env.OUTPOST_API_KEY,
});

const listTopics = async () => {
  const subscriptions = db.getSubscriptions();
  const allTopics = new Set<string>();

  for (const subscription of subscriptions) {
    for (const topic of subscription.topics) {
      allTopics.add(topic);
    }
  }

  return Array.from(allTopics);
};

const migrateOrganizations = async () => {
  const migratedOrgIds: string[] = [];
  const organizations = db.getOrganizations();
  for (const organization of organizations) {
    await outpost.tenants.upsert(organization.id);
    migratedOrgIds.push(organization.id);
  }
  return migratedOrgIds;
};

const migrateSubscriptions = async (organizationId: string) => {
  const subscriptions = db.getSubscriptions(organizationId);
  for (const subscription of subscriptions) {
    await outpost.destinations.create(organizationId, {
      type: "webhook",
      config: {
        url: subscription.url,
      },
      topics: subscription.topics,
      credentials: {
        secret: subscription.secret,
      },
    });
  }
};

const main = async () => {
  const topics = await listTopics();
  console.log("Subscription topics:", topics);

  const migratedOrgIds = await migrateOrganizations();

  for (const organizationId of migratedOrgIds) {
    await migrateSubscriptions(organizationId);
  }
};

main()
  .then(() => {
    console.log("Migration complete");
    process.exit(0);
  })
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

A runnable variant lives at examples/demos/nodejs/src/migrate.ts. The examples/sdk-typescript package pins the SDK via file:../../sdks/outpost-typescript for working inside this repository; when you copy the script elsewhere, depend on @hookdeck/outpost-sdk from npm.

Troubleshooting