API Webhooks

Current outbound webhook delivery contract for Leenops modules.

Leenops webhooks are module-owned outbound HTTP callbacks. They are not a single global API surface: availability, configuration, scope, and event names depend on the module.

Current model

When a module emits an event, Leenops:

  1. Finds enabled webhook subscriptions in the same workspace whose events array contains the emitted event.
  2. Applies module scoping when the module configured a scope column, such as project_id, contract_id, or booking_link_id.
  3. Queues one delivery per matching subscription.
  4. A background dispatcher processes due deliveries.
  5. Each delivery is sent as an HTTP POST to the subscription URL.

Webhook enqueue is additive. In the audited modules, business actions generally continue even if webhook enqueue fails.

Shipped module bindings

ModuleSupported scope
ProjectsWorkspace or project
TimeWorkspace
SupportWorkspace
SchedulingWorkspace or booking link
ContractsWorkspace or contract
ProcurementWorkspace
PlanningWorkspace
GoalsWorkspace
SurveysWorkspace
SubscriptionsWorkspace
MarketingWorkspace

Some modules have richer user-facing webhook settings than others. Treat webhook availability as module-specific and verify it in the module guide or workspace settings.

Delivery payload

Outbound deliveries are JSON.

{
  "id": "delivery_123",
  "event": "task.updated",
  "workspace_id": "00000000-0000-0000-0000-000000000000",
  "created_at": "2026-06-27T12:00:00.000Z",
  "payload": {
    "id": "task_123"
  }
}
FieldDescription
idDelivery row id. Use this for idempotency.
eventModule event name that matched the subscription.
workspace_idWorkspace that emitted the event.
created_atDelivery row creation time.
payloadModule-specific event payload.

The payload shape is event-specific. Do not assume the generic CRM/recruiting/finance examples from older docs apply to every module.

Request headers

Every delivery includes:

HeaderValue
Content-Typeapplication/json
X-Leenops-Signaturesha256=<hmac_hex>
X-Leenops-EventEvent name
X-Leenops-Delivery-IdDelivery row id
X-Leenops-TimestampUnix timestamp in seconds
X-Leenops-ModuleModule binding name, such as projects or support

Signature verification

Leenops signs this string:

{timestamp_seconds}.{raw_json_body}

The signature is HMAC-SHA256 using the subscription secret, encoded as:

sha256=<hex>

Recommended receiver checks:

  1. Read the raw request body before JSON parsing.
  2. Reject timestamps older than about five minutes.
  3. Compute HMAC-SHA256 over {timestamp}.{body} with the subscription secret.
  4. Constant-time compare the computed sha256=<hex> value with X-Leenops-Signature.
  5. Store X-Leenops-Delivery-Id or the payload id to make processing idempotent.
import crypto from "node:crypto";

function verifyLeenopsWebhook(opts: {
  body: string;
  timestamp: string;
  signature: string | null;
  secret: string;
}): boolean {
  if (!opts.signature?.startsWith("sha256=")) return false;
  const signed = `${opts.timestamp}.${opts.body}`;
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", opts.secret).update(signed).digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(opts.signature),
  );
}

Retry behavior

The shared engine currently uses this policy:

OutcomeBehavior
HTTP 2xxMark delivery delivered; clear next_attempt_at.
HTTP 429Retry.
HTTP 5xxRetry.
Network error or timeoutRetry.
Other HTTP 4xxPermanent failure.
Webhook disabled before deliveryMark failed with webhook disabled.

Attempts are capped at four total tries: the first attempt plus three retries. Retry delays are approximately:

Failed attemptNext delay
11 minute
25 minutes
330 minutes
4Permanent failure

Each request times out after 10 seconds by default.

Delivery cadence

Delivery is asynchronous and can take up to 24 hours on the current service tier. Retry delays are minimum delays; the next dispatcher run determines the actual attempt time.

Receivers must deduplicate by X-Leenops-Delivery-Id. Leenops coordinates concurrent delivery processing, but network timeouts can still make the same event visible more than once.

Event examples by module

These event names are documented in code comments or emitted by current module actions. Module availability can differ by workspace and rollout status.

ModuleExample events
Projectstask.updated from bulk issue updates; project-scoped or workspace-wide subscriptions.
Timetime_entry.created, time_entry.updated, time_entry.deleted, timer.started, timer.stopped, timesheet.submitted, timesheet.approved, timesheet.rejected, calendar_import.converted, invoice_lines.created.
SupportSupport ticket events emitted by support services and inbound email handling, including ticket creation/customer reply flows.
SchedulingBooking lifecycle events such as created, approved, declined, cancelled, rescheduled, no-show, and payment-related events.
ContractsContract lifecycle and public-view events emitted from contract actions and renewal cron.
Procurementpurchase_order.approved, vendor_bill.paid.
Planningallocation.created, allocation.updated, allocation.deleted, allocation.confirmed, role_placeholder.created, role_placeholder.staffed, capacity.alert.
Goalsgoal.created, goal.status_changed, goal.closed, cycle.opened, cycle.closed.
Surveyssurvey.response_submitted, survey.completed.
Subscriptionssubscription.created, subscription.updated, subscription.cancelled, subscription.payment_failed, subscription.churned, subscription.plan_changed.
Marketingcampaign.scheduled, campaign.sending, campaign.sent, campaign.bounce_threshold_breached, send.opened, send.clicked, send.unsubscribed, send.bounced, segment.resolved.

What not to assume

  • Do not assume webhooks are configured from a single Settings > API > Webhooks page.
  • Do not assume static Leenops source IP ranges are published.
  • Do not assume five retries; the shared engine caps at four total attempts.
  • Do not assume failed webhooks automatically disable the subscription.
  • Do not assume every event in older docs exists on the current /api/v1 surface.

Receiver checklist

  1. Respond with a 2xx status only after the delivery is durably accepted.
  2. Return 429 or 5xx for transient failures you want retried.
  3. Return other 4xx statuses only for permanent failures.
  4. Verify signatures against the raw body and timestamp.
  5. Deduplicate by delivery id.
  6. Keep handlers below the 10-second timeout; process long work asynchronously.