Appearance
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 → Integrations → Custom ingest → Manual webhook:
Preferred (keeps tokens out of proxy access logs):
POST https://exhale.kolstromsystems.com/webhooks/manualSend 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, andmessage.
Unknown tokens return 404. Disabled or expired-trial tenants return 403.
Customer setup
- Sign in as admin → Connections → Integrations → Custom ingest → Manual webhook.
- Click Connect — the signing secret is shown once; copy it to your sender.
- Copy the webhook URL from the card. Use View docs for the JSON envelope and curl examples (
external_idrequired). - Optional: Edit settings to configure Field path mapping for nested JSON (dot notation, e.g.
alert.id), or rotate the signing secret. - Send test alert creates a synthetic pulse and marks the integration configured.
Rotate signing secret invalidates the previous secret immediately.
Default JSON envelope
| Field | Required | Default path | Notes |
|---|---|---|---|
external_id | Yes | external_id | Stable id for dedupe (source=other) |
title | No | title | Defaults to Manual alert {external_id} |
event_type | No | event_type | triggered (default), acknowledged, or resolved |
source_url | No | source_url | Deep link in the UI (https only in staging/production) |
delivery_id | No | delivery_id | Replay 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-pathsPUT /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>.
| Environment | Behavior |
|---|---|
| Local / test | Signature optional when no signing secret is stored |
| Staging / production | 401 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-Length→ 400 - 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_type | Pulse status | Triage |
|---|---|---|
triggered (default) | received | Enqueued when async triage is enabled |
acknowledged | acknowledged | No new triage enqueue |
resolved | resolved | No 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/*.
| Route | Purpose |
|---|---|
POST …/connect | Issue URL token + signing secret (once) |
POST …/rotate-secret | New signing secret (once) |
POST …/test-alert | Synthetic ingest + mark configured |
GET / PUT …/field-paths | Read/update dot-path mapping |
GET /auth/integrations includes webhook_path for connected manual webhooks; never exposes the signing secret.