Skip to content

Manual webhook

Tenant manual webhooks let customers POST arbitrary JSON when no vendor adapter exists — internal runbooks, glue scripts, or custom monitoring. Each tenant gets a unique HTTPS URL and HMAC signing secret.

See Incoming integrations for how manual webhooks fit alongside PagerDuty.

OpenAPI: POST /webhooks/manual (header token) and POST /webhooks/manual/{token} — security scheme ExhaleSignature.


Endpoint

After Connect in Connections → IntegrationsCustom ingestManual webhook:

Preferred (keeps tokens out of proxy access logs):

POST https://exhale.kolstromsystems.com/webhooks/manual

Send header X-Exhale-Webhook-Token: {token}.

Legacy path form (still supported):

POST https://exhale.kolstromsystems.com/webhooks/manual/{token}

Design partners may substitute https://exhale-staging.kolstromsystems.com as the host.

  • token — opaque URL segment (not a secret; the signing secret authenticates deliveries).
  • Response: 202 Accepted with pulse_id, status, created, and message.

Unknown tokens return 404. Disabled or expired-trial tenants return 403.


Customer setup

  1. Sign in as adminConnections → IntegrationsCustom ingestManual webhook.
  2. Click Connect — the signing secret is shown once; copy it to your sender.
  3. Copy the webhook URL from the card. Use View docs for the JSON envelope and curl examples (external_id required).
  4. Optional: Edit settings to configure Field path mapping for nested JSON (dot notation, e.g. alert.id), or rotate the signing secret.
  5. Send test alert creates a synthetic pulse and marks the integration configured.

Rotate signing secret invalidates the previous secret immediately.


Default JSON envelope

FieldRequiredDefault pathNotes
external_idYesexternal_idStable id for dedupe (source=other)
titleNotitleDefaults to Manual alert {external_id}
event_typeNoevent_typetriggered (default), acknowledged, or resolved
source_urlNosource_urlDeep link in the UI (https only in staging/production)
delivery_idNodelivery_idReplay dedupe id; may also be sent as header

Signed curl example

bash
TOKEN="your-url-token"
SECRET="your-signing-secret"
BODY='{"external_id":"MANUAL-1","title":"Disk full on db-01","event_type":"triggered"}'
SIG="v1=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

# Preferred — token in header
curl -i -X POST "https://exhale.kolstromsystems.com/webhooks/manual" \
  -H "Content-Type: application/json" \
  -H "X-Exhale-Webhook-Token: ${TOKEN}" \
  -H "X-Exhale-Signature: $SIG" \
  -d "$BODY"

# Legacy — token in URL path
curl -i -X POST "https://exhale.kolstromsystems.com/webhooks/manual/${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Exhale-Signature: $SIG" \
  -d "$BODY"

Field path mapping

When payloads nest fields, admins configure dot-paths in the Integrations UI or via:

  • GET /auth/integrations/manual-webhook/field-paths
  • PUT /auth/integrations/manual-webhook/field-paths (admin + CSRF)

Example paths:

json
{
  "external_id": "alert.id",
  "title": "alert.summary",
  "event_type": "alert.status",
  "source_url": "alert.url",
  "delivery_id": "meta.delivery_id"
}

Exhale reads each field only from its configured path. Missing required paths return 400.


Signature verification

Deliveries must include X-Exhale-Signature: HMAC-SHA256 of the raw body as v1=<hex>.

EnvironmentBehavior
Local / testSignature optional when no signing secret is stored
Staging / production401 when signature missing or invalid

There is no timestamp in the signature header — use delivery_id (body or X-Exhale-Delivery-Id header) for replay protection.


Dedupe and reliability

Same pipeline limits as PagerDuty ingest:

  • Bodies larger than the configured maximum (default 1 MiB) → 413
  • Invalid or (in staging/production) missing Content-Length400
  • Per-IP rate limits when configured → 429 with Retry-After
  • Redis fail-closed (staging/production) can return 503 when dedupe storage is unavailable

Lifecycle mapping

event_typePulse statusTriage
triggered (default)receivedEnqueued when async triage is enabled
acknowledgedacknowledgedNo new triage enqueue
resolvedresolvedNo new triage enqueue

Duplicate (tenant_id, source=other, external_id) updates the existing row and returns 202 with created: false.


Scrubbing and logging

Parsed JSON is scrubbed before persist (same pipeline as PagerDuty). Application logs record pulse_id, correlation_id, source, and external_id only — never the raw body.

Stored pulses use source=other; the UI labels them Manual webhook.


Auth API (customer)

Admin session + CSRF on mutating routes. See API reference for /auth/integrations/manual-webhook/*.

RoutePurpose
POST …/connectIssue URL token + signing secret (once)
POST …/rotate-secretNew signing secret (once)
POST …/test-alertSynthetic ingest + mark configured
GET / PUT …/field-pathsRead/update dot-path mapping

GET /auth/integrations includes webhook_path for connected manual webhooks; never exposes the signing secret.


Exhale by Kolstrom Systems LLC