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 https://www.useharbor.io/v1/webhook_endpoints \
-H "Authorization: Bearer $HARBOR_KEY" \
-H "content-type: application/json" \
-d '{ "url": "https://yourapp.com/harbor/webhooks" }'{ "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.
{
"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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdv1 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);
}import hashlib, hmac, time
def verify_harbor(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if not t or abs(time.time() - t) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Delivery and retries
- Each event is sent as a
POSTwith a JSON body as soon as the change happens. Return any2xxwithin 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
idto 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
httpsURLs. Private addresses,localhostand internal hostnames are rejected.
Test and debug
In the dashboard's Webhooks page:
- Send test posts a
webhook.testevent 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.
{
"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." }
}