Outbound Webhooks

Outbound webhooks are HTTPS endpoints you own that Hiveship POSTs to whenever a subscribed workspace event fires — issues created, comments updated, agent sessions completed. Pair them with a Personal Access Token: webhooks tell your service when something happened; PATs let it fetch the full entity to act on it. Available on the PRO, TEAM, and ENTERPRISE plans.

Subscribing

  1. Open Workspace settings → Webhooks in the dashboard.
  2. Click Add webhook. Enter the receiver URL (must be HTTPS), pick the events you care about, and save.
  3. Copy the signing secret immediately. Like PAT plaintext, the secret is shown once. Store it in your secret manager.
  4. Verify deliveries are reaching your service via the per-webhook Deliveries tab. The first event after creation typically arrives within a second.

Delivery shape

Every delivery is an application/json POST with three signing headers and a stable user-agent. Payloads contain entity IDs only — fetch the full record with your PAT when you need more.

http
POST https://yourapp.example.com/hooks/hiveship
Content-Type: application/json
User-Agent: Hiveship-Webhooks/1.0
X-Hiveship-Signature-Timestamp: 1717250000
X-Hiveship-Signature: sha256=abcd...64-hex-chars...
{
"event": "issue.created",
"payload": {
"projectId": "clx...",
"issueId": "cly...",
"parentId": null
},
"deliveryId": "clz...",
"webhookId": "clw...",
"timestamp": "2026-05-17T12:00:00.123Z"
}

Respond with any 2xx within 10 seconds to acknowledge a delivery. Non-2xx responses, timeouts, and connection errors trigger retries (see below). Hiveship never follows redirects — a 3xx is treated as failure. If your handler does heavy work, push it onto a queue and return 200 immediately rather than holding the connection.

Verifying signatures

The X-Hiveship-Signature header is an HMAC-SHA256 of ${timestamp}.${rawBody} using your webhook's signing secret. The scheme mirrors Stripe — the timestamp prefix binds the signature to a moment in time, which blocks replay attacks.

Reject any request whose timestamp is more than five minutes off the wall clock, even if the signature matches. Without that check, a captured payload can be replayed indefinitely.

verify.tstypescript
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyHiveshipWebhook(
secret: string,
rawBody: string,
headers: Record<string, string>,
): boolean {
const ts = Number(headers['x-hiveship-signature-timestamp']);
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) {
return false;
}
const expected =
'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const got = headers['x-hiveship-signature'] ?? '';
if (got.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}

The recipe is the same in every language: HMAC-SHA256 the ${ts}.${body} string with the secret, hex-encode, prefix with sha256=, and compare in constant time against the header.

Capture the raw request body, not the parsed JSON — re-serializing changes whitespace and key ordering, and the signature won't match. In Express this means a bodyParser.raw() middleware on your webhook route; in Next.js, disable the built-in body parser for the route.

Events

The webhook event vocabulary is a curated subset of Hiveship's internal event stream — high-signal entity changes only. The firehose (per-keystroke agent activity) and per-user notifications are deliberately excluded; both would generate too much traffic to be useful in a webhook pipeline.

GroupEvents
Issuesissue.created, issue.updated, issue.deleted, issue.pr_linked, issue.pr_unlinked, issue.pr_status_changed
Commentscomment.created, comment.updated, comment.deleted
Projectsproject.created, project.updated, project.deleted
Sprintssprint.created, sprint.updated, sprint.deleted
Membersmember.added, member.removed
Agent sessionsagent_session.created, agent_session.completed, agent_session.errored

Retries + auto-disable

A delivery that returns non-2xx, times out, or errors at the network layer is retried with exponential backoff: 10s, 20s, 40s, 80s, 160s. Five attempts total; ~5 minutes wall-clock worst case before the delivery is marked DEAD.

Each retry signs with the secret that was active at enqueue time. If you rotate a secret mid-flight, in-flight retries continue with the old secret — receivers should accept both the old and new secret for ~5 minutes after rotation to avoid dropping the tail of the retry envelope.

After 15 consecutive terminal failures with no successful delivery in between, Hiveship disables the webhook. Workspace admins see a banner in the settings page; flipping it back to active clears the failure counter. The threshold is intentional — a dead endpoint costs Hiveship retry budget across the cluster, and the operator who configured it deserves a fast feedback loop.

Rotating the signing secret

Rotate from Workspace settings → Webhooks on the webhook's row. The new secret takes effect for future deliveries; the old secret is retired immediately for new enqueues. The new plaintext is shown once in the rotate dialog — copy it into your secret manager before closing.

Existing in-flight retries continue signing with the old secret (each delivery captures the secret at enqueue time and walks the retry envelope with it). The longest your receiver might see an old-secret-signed request is ~5 minutes — the worst-case tail of the 10s / 20s / 40s / 80s / 160s exponential backoff. Configure your receiver to accept both the old and new secret for at least 5 minutes after each rotation; retire the old secret only after that window closes.

Verify against both candidates and pass on the first match:

rotation-aware verifiertypescript
function verifyEitherSecret(
secrets: { current: string; previous?: string },
rawBody: string,
headers: Record<string, string>,
): boolean {
if (verifyHiveshipWebhook(secrets.current, rawBody, headers)) return true;
if (secrets.previous && verifyHiveshipWebhook(secrets.previous, rawBody, headers)) {
return true;
}
return false;
}

There is no rotate flow for Personal Access Tokens — revoke and issue a new one. Webhook secrets get the rotation flow because the receiver typically can't be redeployed in lockstep with a new secret; the grace window covers that gap.

Replay

The Deliveries tab keeps the last 200 events per webhook, regardless of outcome. Click Replay on any past delivery (including DEAD ones) to re-send the original payload — useful for testing receiver changes against real historic traffic, or for catching up after an extended outage.

The replayed event reuses the original signing secret (the secret active at the delivery's enqueue time, not the secret active now). If you've rotated since the original delivery, accept the old secret long enough to verify replays during your outage recovery, then retire it.

Best practices

  • Verify the signature before any processing. Don't log, don't parse, don't queue — verify first.
  • Treat deliveries as at-least-once. Retries happen; deduplicate by deliveryId on your side if the work isn't idempotent.
  • Acknowledge fast, process async. Hiveship times out at 10 seconds. Drop the delivery into a queue and return 200 immediately if processing takes longer than a quick database write.
  • Don't gate on Origin or Referer. Server-to-server deliveries don't carry browser headers. Gate on the signature instead.
  • One webhook per receiver. If two pipelines need the same event, subscribe two webhooks rather than fanning out on your side — Hiveship's delivery log then tells you which pipeline failed.

Plan + cap

Each workspace can run up to 20 active webhooks on PRO and TEAM; ENTERPRISE inherits the same per-workspace cap but with the higher PAT rate-limit tier on the receivers that fetch entities. If a workspace downgrades to FREE, existing webhooks keep delivering — webhook auth doesn't re-check the plan flag — but new ones cannot be created.

Live reference

The webhook management endpoints (GET / POST / PATCH / DELETE /api/workspaces/:wId/webhooks, plus replay and rotate-secret) are documented inside the live Swagger UI under the Webhooks tag. Open Swagger UI for the full surface.