Task Ember provides webhook endpoints for receiving alerts from external services.
Send an alert to the Inbox via webhook.
Endpoint:
POST https://apim-taskember.azure-api.net/webhook/{databaseId}/{webhookId}
Headers:
| Header | Required | Description |
|---|---|---|
x-api-key | Yes | Your webhook API key |
Content-Type | Yes | Must be application/json |
x-taskember-signature | Yes | sha256=<hex> HMAC of the request (see Signing requests) |
x-taskember-timestamp | Yes | Unix time in seconds when the request was signed |
x-idempotency-key | Recommended | Unique id for this delivery. It is signed, and duplicates are dropped by it. Always send it; x-event-id is used only when it is absent |
Webhooks created in the portal require a signature. Compute an HMAC-SHA256 over
{timestamp}\n{deliveryId}\n{rawBody}
that is, the timestamp, a newline, the delivery id, a newline, then the exact
bytes of the body you send (not a re-serialised copy). Key it with the
webhook's signing secret and send it hex-encoded as
x-taskember-signature: sha256=<hex> alongside the x-taskember-timestamp you
signed with. The API key still goes in x-api-key, but it is never the signing
key.
The delivery id is the trimmed value of x-idempotency-key, or of
x-event-id when that is absent. With neither it is empty, so the signed
string starts {timestamp}\n\n. x-request-id is never used, because proxies
can add or rewrite it. A delivery id containing a line break is rejected.
Always send x-idempotency-key with a new unique value (such as a UUID) for
each delivery, and reuse it only when retrying the same one: Task Ember drops
duplicate deliveries by this id.
A request is rejected when the signature is missing, does not match, or the timestamp is more than 300 seconds away from the receiver's clock. A webhook with signing turned off accepts requests on the API key alone, but a signature that is present and wrong is still rejected.
The signing secret is shown once, when the webhook is created. Rotate signing secret on the webhook card replaces it, and the old secret stops working immediately.
Webhooks without a signing secret: webhooks created before signing secrets
existed are verified with the older scheme until their owner clicks Generate
signing secret: an HMAC-SHA256 over {timestamp}.{rawBody}, keyed with the
API key. Generate a signing secret to move off it, since the API key travels
with every request.
TIMESTAMP=$(date +%s)
DELIVERY_ID=$(uuidgen)
BODY='{"title":"Server CPU High","severity":"warning"}'
SIGNATURE=$(printf '%s\n%s\n%s' "$TIMESTAMP" "$DELIVERY_ID" "$BODY" \
| openssl dgst -sha256 -hmac "$SIGNING_SECRET" -hex \
| sed 's/^.* //')
curl -X POST "$WEBHOOK_URL" \
-H "x-api-key: $API_KEY" \
-H "x-idempotency-key: $DELIVERY_ID" \
-H "x-taskember-timestamp: $TIMESTAMP" \
-H "x-taskember-signature: sha256=$SIGNATURE" \
-H "Content-Type: application/json" \
-d "$BODY"
Request Body:
{
"title": "Alert title",
"severity": "critical",
"message": "Optional detailed description",
"source": "Your service name",
"metadata": {
"key": "value"
}
}
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Alert title (defaults to "Webhook Alert") |
severity | string | No | One of critical, warning, info (defaults to info) |
message | string | No |
Response:
{
"success": true,
"alertId": "wh-abc123-def456"
}
Error Responses:
| Status | Description |
|---|---|
| 400 | Invalid JSON body or invalid severity value |
| 401 | Signature missing or invalid (for example signed with the API key instead of the signing secret), or timestamp outside the 300 second window |
| 403 | Webhook is disabled |
| 404 | Webhook not found |
| 500 | Internal server error |
WEBHOOK_URL="https://apim-taskember.azure-api.net/webhook/YOUR_DATABASE_ID/YOUR_WEBHOOK_ID"
API_KEY="YOUR_API_KEY"
SIGNING_SECRET="YOUR_SIGNING_SECRET"
TIMESTAMP=$(date +%s)
DELIVERY_ID=$(uuidgen)
BODY='{"title":"Server CPU High","severity":"warning","message":"CPU usage at 95% on prod-web-01","source":"monitoring-agent","metadata":{"host":"prod-web-01","region":"uk-south","cpu_percent":"95"}}'
SIGNATURE=$(printf '%s\n%s\n%s' "$TIMESTAMP" "$DELIVERY_ID" "$BODY" \
| openssl dgst -sha256 -hmac "$SIGNING_SECRET" -hex \
| sed 's/^.* //')
curl -X POST "$WEBHOOK_URL" \
-H "x-api-key: $API_KEY" \
-H "x-idempotency-key: $DELIVERY_ID" \
-H "x-taskember-timestamp: $TIMESTAMP" \
-H "x-taskember-signature: sha256=$SIGNATURE" \
-H "Content-Type: application/json" \
-d "$BODY"
$apiKey = "YOUR_API_KEY"
$signingSecret = "YOUR_SIGNING_SECRET"
$body = @{
title = "Backup Complete"
severity = "info"
message = "Nightly backup finished successfully"
source = "backup-service"
metadata = @{
database = "prod-db-01"
duration = "12 minutes"
size = "2.4 GB"
}
} | ConvertTo-Json -Compress
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
# Always send a delivery id: it is signed, and retries of the same delivery reuse it
$deliveryId = [guid]::NewGuid().ToString()
$hmac = [System.Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($signingSecret))
$hash = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes("$timestamp`n$deliveryId`n$body"))
$signature = -join ($hash | ForEach-Object { $_.ToString("x2") })
Invoke-RestMethod `
-Uri "https://apim-taskember.azure-api.net/webhook/YOUR_DATABASE_ID/YOUR_WEBHOOK_ID" `
-Method Post `
-Headers @{
"x-api-key" = $apiKey
"x-idempotency-key" = $deliveryId
"x-taskember-timestamp" = $timestamp
"x-taskember-signature" = "sha256=$signature"
} `
-ContentType "application/json" `
-Body $body
import requests
response = requests.post(
"https://apim-taskember.azure-api.net/webhook/YOUR_DATABASE_ID/YOUR_WEBHOOK_ID",
headers={
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"title": "Deployment Failed",
"severity": "critical",
"message": "Production deployment rolled back due to health check failure",
"source": "ci-pipeline",
"metadata": {
"pipeline": "main-deploy",
"commit": "abc1234",
"environment": "production",
},
},
)
print(response.json())
const response = await fetch(
"https://apim-taskember.azure-api.net/webhook/YOUR_DATABASE_ID/YOUR_WEBHOOK_ID",
{
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "New User Signup",
severity: "info",
message: "user@example.com signed up",
source: "auth-service",
metadata: {
email: "user@example.com",
plan: "pro",
},
}),
}
);
const data = await response.json();
console.log(data);
Configure a service hook in Azure DevOps to send build/release notifications to Task Ember:
Add a step to your workflow to notify Task Ember on deployment:
- name: Notify Task Ember
if: always()
run: |
curl -X POST "$WEBHOOK_URL" \
-H "x-api-key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Deploy ${{ job.status }}\",
\"severity\": \"${{ job.status == 'success' && 'info' || 'critical' }}\",
\"source\": \"github-actions\",
\"metadata\": {
\"repo\": \"${{ github.repository }}\",
\"commit\": \"${{ github.sha }}\"
}
}"
env:
WEBHOOK_URL: ${{ secrets.TASKEMBER_WEBHOOK_URL }}
API_KEY: ${{ secrets.TASKEMBER_API_KEY }}
Configure a contact point in Grafana to send alerts to Task Ember:
x-api-key as a custom headerWebhook requests are rate-limited through Azure API Management. If you exceed the limit, you will receive a 429 Too Many Requests response.
title values so alerts are easy to scan in the Inboxseverity appropriately - reserve critical for genuine outagesmetadata for troubleshooting (host names, error codes, etc.)source field so you can filter alerts by service| Detailed alert description |
source | string | No | Name of the service sending the alert |
metadata | object | No | Key-value pairs for additional context |