Skip to main content
Version: Next (Unreleased)

Webhook delivery

When an event matches a registered endpoint's filter, Harbor sends an HTTPS POST to your URL. Deliveries are signed, retried on failure, and logged in the dashboard.

Delivery flow

Each delivery is independent. If you register three endpoints for order.shipped, Harbor sends three requests.

Delivery payload

Harbor wraps the event in an envelope:

{
"id": "del_0193f1a2c001",
"type": "event.created",
"created_at": "2026-05-25T12:00:01Z",
"data": {
"id": "evt_02m4k9p1q7w3",
"type": "order.shipped",
"workspace_id": "ws_018f3a2e4b9c",
"payload": {
"order_id": "ord_0192be7a3c4f",
"carrier": "ups"
},
"created_at": "2026-05-25T12:00:00Z"
}
}

Use data.id for deduplication. The delivery ID (del_ prefix) identifies a specific attempt sequence, not the underlying event.

Headers

HeaderPurpose
Harbor-SignatureHMAC signature of {timestamp}.{body}
Harbor-TimestampUnix timestamp when Harbor signed the payload
Harbor-Delivery-IdUnique ID for this delivery attempt

Verify signatures before parsing JSON. verifyWebhookSignature() rejects timestamps older than five minutes. Full handler example in Webhooks guide.

Retries and timeouts

Harbor waits up to 10 seconds for a response. Non-2xx status codes and connection failures trigger retries with exponential backoff. See the retry schedule in Webhooks guide § Delivery retries. Retries stop after 72 hours total.

Your endpoint must be idempotent: the same event may arrive more than once after retries or manual replay.

At-least-once semantics

Harbor guarantees at-least-once delivery, not exactly-once. Store processed event IDs in your database and skip duplicates:

async function handleDelivery(envelope: DeliveryEnvelope) {
const eventId = envelope.data.id;
if (await db.processedEvents.has(eventId)) return;
await processEvent(envelope.data);
await db.processedEvents.add(eventId);
}

Next steps

Continue to Webhooks guide. Verification issues in Common errors.