harbordocs Get API keys

Webhooks

Harbor posts an event to your server whenever a checkout, order or connection changes, signed so you can trust it.

Add an endpoint

In the dashboard, open Webhooks and add an https URL. Or with the API:

curl
curl https://www.useharbor.io/v1/webhook_endpoints \
  -H "Authorization: Bearer $HARBOR_KEY" \
  -H "content-type: application/json" \
  -d '{ "url": "https://yourapp.com/harbor/webhooks" }'
json
{ "id": "whk_…", "url": "https://yourapp.com/harbor/webhooks", "secret": "whsec_…" }

Keep the secret. Sandbox and live keys have separate endpoints and secrets.

Events

Every event has the same envelope. data is the full object after the change.

json
{
  "id": "evt_6j0n1e4h3k232733243s",
  "type": "checkout.completed",
  "created_at": "2026-09-24T18:02:11.000Z",
  "data": { "id": "chk_162g7258534z6g3e1o5k", "status": "completed", "order_id": "ord_…" }
}
Type Sent when data
checkout.updated Any change to a checkout Checkout
checkout.requires_user_action A checkout needs the user or you Checkout
checkout.completed The order was placed Checkout
checkout.failed The checkout can't go through Checkout
order.updated An order was placed or changed status Order
connection.needs_user A store connection needs the user Connection
connection.revoked The user disconnected a store Connection
webhook.test You pressed Send test in the dashboard A test object

The specific checkout events arrive alongside checkout.updated for the same change, so handle one or the other.

Verify the signature

Every delivery has a Harbor-Signature header:

Harbor-Signature: t=1790227763,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 is the hex HMAC-SHA256 of {t}.{raw body} with your endpoint secret. Compute it over the raw body exactly as received, compare in constant time, and reject old timestamps.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyHarbor(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  const want = Buffer.from(expected, "hex");
  return given.length === want.length && timingSafeEqual(given, want);
}

Delivery and retries

  • Each event is sent as a POST with a JSON body as soon as the change happens. Return any 2xx within 10 seconds and do the work afterwards.
  • Anything else counts as a failure: another status code, a timeout, a network error, or a redirect (Harbor doesn't follow them).
  • Failed deliveries are retried after about 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours: seven attempts over roughly a day and a half. Each attempt is signed again with a fresh timestamp.
  • Events can arrive more than once and out of order. Use the event id to ignore duplicates, and read the object back (GET /v1/checkouts/{id}, GET /v1/orders/{id}) when order matters; it's always current.
  • Endpoints must be public https URLs. Private addresses, localhost and internal hostnames are rejected.

Test and debug

In the dashboard's Webhooks page:

  • Send test posts a webhook.test event to one endpoint so you can check your handler and signature code.
  • Deliveries lists every attempt with the response status or error, and when the next retry is.
  • Resend delivers any event again right away.
json
{
  "id": "evt_…",
  "type": "webhook.test",
  "created_at": "2026-09-24T18:00:00.000Z",
  "data": { "id": "test_1a2b3c4d", "message": "A test event from the Harbor dashboard." }
}