Horizon

Test Airship webhooks locally

Receive Airship Real-Time Data Streaming webhook events on localhost with a Horizon tunnel, and protect the endpoint with a custom header secret.

Receive Airship events on your laptop while you build, with a public HTTPS URL Airship can reach.

Horizon has no Airship integration. Airship posts events to a public URL, and Horizon provides that URL.

Airship doesn't sign webhook requests. Its docs describe no signature header. The protection Airship documents is custom headers: you add a key and value in the webhook settings, and Airship sends them with each request. This guide uses one as a shared secret.

Before you begin

  • Node.js 18 or later
  • A Horizon account and the CLI (see Getting started)
  • A reserved subdomain for -s. Reserve one on the Subdomains page.
  • An Airship project with Real-Time Data Streaming enabled. It is an add-on service, so contact Airship Sales if it is off.
  • A Next.js app that uses the App Router and runs on port 3000

Start your app

Airship posts events as JSON. By default, one request holds an array of 10 event objects. Each object has fields such as id, occurred and device.

The handler checks a custom header against a secret you choose. It compares in constant time.

app/api/webhooks/airship/route.ts
import { timingSafeEqual } from "node:crypto";

export async function POST(request: Request) {
  const secret = process.env.AIRSHIP_WEBHOOK_SECRET;
  if (!secret) {
    return new Response("Missing AIRSHIP_WEBHOOK_SECRET", { status: 500 });
  }

  const received = Buffer.from(request.headers.get("x-webhook-secret") ?? "");
  const expected = Buffer.from(secret);
  const isValid = received.length === expected.length && timingSafeEqual(received, expected);

  if (!isValid) {
    return new Response("Invalid secret", { status: 401 });
  }

  const events = (await request.json()) as { id: string }[];
  console.log(`Received ${events.length} Airship event(s)`);

  return new Response("ok", { status: 200 });
}

X-Webhook-Secret is a name this guide picked. Airship doesn't define it. Pick a long random value for the secret.

.env.local
AIRSHIP_WEBHOOK_SECRET=replace-with-a-long-random-string

Start the app.

npm run dev

Start a tunnel

In a second terminal, open a tunnel on a subdomain you reserved.

hrzn tunnel http://localhost:3000 -s my-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:3000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Your public URL is https://my-app.hrzn.run. Airship requires an HTTPS host, and every Horizon tunnel has one. Keep this terminal open.

Use -s. Without it, the subdomain is random and changes every run, so your Airship webhook would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.

Add the webhook in Airship

  1. In the Airship dashboard, open the dropdown next to your project name, then select Settings.
  2. Under Project settings, select Partner Integrations.
  3. Select Webhook.
  4. Enter a name and description.
  5. Set Webhook request URL to https://my-app.hrzn.run/api/webhooks/airship.
  6. Under Custom headers, add the key X-Webhook-Secret and the same value as AIRSHIP_WEBHOOK_SECRET.
  7. Choose a Batch size. A single object per request is easiest to read while you build.
  8. Under Event types, check the events you want.
  9. Select Activate.

Trigger an event

The Airship docs describe no test button or resend for webhook integrations. Cause a real event for an event type you selected, for example by opening a message on a test device that belongs to your project.

Check it works

The Horizon terminal prints one line per request:

Output
  POST    200  /api/webhooks/airship

Your app terminal prints Received 1 Airship event(s), or more if you picked a larger batch size.

Troubleshooting

The route returns 401

The Horizon line shows [401]. The header value didn't match.

  • Check that the Custom headers key is X-Webhook-Secret and the value is identical to AIRSHIP_WEBHOOK_SECRET, with no extra spaces.
  • Restart npm run dev after you edit .env.local.

Nothing reaches your app

  • Check that the integration shows as active under Partner Integrations.
  • Check that you selected the event type you trigger.
  • Check that the Webhook request URL ends with /api/webhooks/airship.
  • Check that the Horizon terminal is still running.

The URL changed after a restart

You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.

Next steps

On this page