Horizon

Test Box webhooks locally

Receive Box V2 webhook events on localhost with a Horizon tunnel, and verify the primary and secondary signatures in a Next.js route handler.

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

Horizon has no Box integration. Box sends webhooks to a public URL, and Horizon provides that URL.

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.
  • A Box application in the Developer Console, with the Manage Webhooks scope enabled and the application authorized
  • A Next.js app that uses the App Router and runs on port 3000

Start your app

Box signs every V2 webhook with up to two keys, a primary and a secondary. Two keys let you rotate one without downtime. Box computes an HMAC-SHA256 (a keyed hash) over the raw body followed by the BOX-DELIVERY-TIMESTAMP value, encodes it as Base64, and sends it in BOX-SIGNATURE-PRIMARY and BOX-SIGNATURE-SECONDARY. A delivery is valid when at least one signature matches.

The official Box Node SDK ships a helper that does the whole check, including the timestamp. Install it:

npm install box-node-sdk

Create the route handler. Read the raw body with request.text(). Don't parse it first, because the signature covers the exact bytes.

app/api/webhooks/box/route.ts
import { WebhooksManager } from "box-node-sdk/managers";

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

  const body = await request.text();
  const headers = Object.fromEntries(request.headers);

  const isValid = await WebhooksManager.validateMessage(body, headers, primaryKey, {
    secondaryKey: process.env.BOX_SECONDARY_KEY,
  });

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

  const event = JSON.parse(body);
  console.log(`Received Box event: ${event.trigger}`);

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

validateMessage rejects a delivery whose BOX-DELIVERY-TIMESTAMP is older than ten minutes, which is the limit Box documents.

Box wants a 2xx response within 30 seconds. Keep the handler fast.

You get the keys in the next steps. Add them to .env.local once you have them:

.env.local
BOX_PRIMARY_KEY=replace-with-your-primary-key
BOX_SECONDARY_KEY=replace-with-your-secondary-key

Start the app:

npm run dev

Start a tunnel

In a second terminal, open a tunnel to port 3000 on a subdomain you reserved, with -s:

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

Your public URL is https://my-box-app.hrzn.run. Without -s the subdomain is random and changes on every run, so the URL you give Box would stop working after a restart. Reserved subdomains are a paid feature, see Pricing.

Generate the signature keys

  1. Open your application in the Developer Console.
  2. Select the Webhooks tab.
  3. Select Manage signature keys.
  4. Select Generate Key to create the primary key. Generate the secondary key the same way.
  5. Copy both values into .env.local as BOX_PRIMARY_KEY and BOX_SECONDARY_KEY, then restart npm run dev.

Without signature keys, Box sends no signature headers and the handler rejects every request.

Create the webhook in Box

Your endpoint URL is the tunnel URL plus the route path: https://my-box-app.hrzn.run/api/webhooks/box. Box requires an HTTPS URL.

  1. Open your application in the Developer Console and select the Webhooks tab.
  2. Select Create webhook.
  3. Select V2 from the drop-down list.
  4. Enter the endpoint URL in URL Address.
  5. Set Content type to the kind of item to watch, a file or a folder, and choose that item.
  6. Select the Triggers you want, for example FILE.UPLOADED.
  7. Select Create webhook to save.

Trigger an event

Box has no resend button for V2 webhooks. Do the thing the trigger watches for. For FILE.UPLOADED on a folder, upload a file to that folder in Box. Box then sends a new delivery to your endpoint.

Check it works

After you upload a file, the terminal that runs hrzn prints one line:

Output
  POST    200  /api/webhooks/box

Your app terminal prints:

Output
Received Box event: FILE.UPLOADED

If the line shows [401], see Troubleshooting.

Troubleshooting

The signature doesn't match

The Horizon line shows [401]. Check these in order:

  • BOX_PRIMARY_KEY and BOX_SECONDARY_KEY match the keys under Manage signature keys, with no extra spaces. Restart npm run dev after you edit .env.local.
  • The handler reads the body with request.text(). Box signs the raw bytes, so parsing and re-serializing the JSON breaks the signature.
  • You generated the keys before you created the webhook. Box adds the signature headers only when keys exist.

A valid delivery is rejected as too old

Box includes the delivery time in BOX-DELIVERY-TIMESTAMP, and the SDK rejects a delivery older than ten minutes. Box retries failed deliveries, so a retry that reaches you late can fail this check. Trigger a fresh event.

The URL changed after a restart

You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-box-app and update URL Address in Box if it differs.

Nothing reaches your app

  • Check that the Horizon terminal and npm run dev are both running.
  • Check that URL Address ends with /api/webhooks/box.
  • Check that you authorized the application and enabled the Manage Webhooks scope. Box creates V2 webhooks only with that scope.

Next steps

On this page