Sentry integration API#
This document describes the IncidentRelay backend API behavior for the Sentry incoming integration.
Endpoint#
POST /api/integrations/sentry/{route_id}
This endpoint receives Sentry Internal Integration webhooks.
Unlike Alertmanager, Zabbix and Generic Webhook integrations, the Sentry endpoint does not use an IncidentRelay intake token. Authentication is based on the route id and Sentry-Hook-Signature verification.
Required headers#
| Header | Required | Description |
|---|---|---|
Content-Type: application/json | Yes | Sentry sends JSON webhook payloads. |
Sentry-Hook-Signature | Yes | HMAC signature generated by Sentry using the Internal Integration Client Secret. |
Sentry-Hook-Resource | Recommended | Sentry resource name, for example event_alert, metric_alert, issue. |
Route requirements#
The route identified by {route_id} must:
- exist;
- have
source=sentry; - be enabled;
- belong to an active team and active group;
- have
integration_config.sentry.webhook_secretconfigured.
Example stored route config:
{
"sentry": {
"webhook_secret": "client-secret-from-sentry"
}
}
API serialization must not return the secret. It should return only:
{
"sentry": {
"has_webhook_secret": true,
"webhook_path": "/api/integrations/sentry/42"
}
}
Signature verification#
IncidentRelay validates the request by calculating an HMAC-SHA256 digest over the raw request body using the route's Sentry webhook secret.
Pseudo-code:
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
valid = hmac.compare_digest(expected, request.headers["Sentry-Hook-Signature"])
The raw request body must be used exactly as received.
Response codes#
| Status | Error | Meaning |
|---|---|---|
404 | route_not_found | No route exists for {route_id}. |
400 | route_source_mismatch | Route exists but is not source=sentry. |
403 | route_disabled | Route is disabled or deleted. |
403 | route_team_inactive | Route team is deleted or inactive. |
403 | route_group_inactive | Route group is deleted or inactive. |
409 | sentry_secret_not_configured | Route has no Sentry Client Secret. |
403 | sentry_signature_missing | Request does not include Sentry-Hook-Signature. |
403 | sentry_signature_invalid | Signature does not match the body and secret. |
400 | validation_error | Request body is not a valid Sentry JSON payload. |
Normalized alert output#
The normalizer returns a list with one IncidentRelay alert object.
Example event_alert.triggered normalized output:
{
"source": "sentry",
"team_slug": null,
"external_id": "12345",
"dedup_key": "sentry:issue:12345",
"title": "ZeroDivisionError",
"message": "division by zero",
"severity": "critical",
"status": "firing",
"labels": {
"alertname": "SentryIssueAlert",
"severity": "critical",
"sentry_resource": "event_alert",
"sentry_action": "triggered",
"organization_slug": "acme",
"organization_name": "Acme",
"project_slug": "backend-api",
"project_name": "Backend API",
"issue_id": "12345",
"issue_short_id": "BACKEND-1",
"event_id": "event-abc",
"environment": "production",
"level": "error",
"sentry_url": "https://sentry.example.com/issues/12345/"
},
"payload": {}
}
Severity mapping#
| Sentry level/status | IncidentRelay severity |
|---|---|
fatal | critical |
critical | critical |
error | critical |
warning | warning |
warn | warning |
info | info |
debug | info |
resolved | info |
ok | info |
| unknown | warning |
Status mapping#
| Sentry resource/action | IncidentRelay status |
|---|---|
event_alert.triggered | firing |
metric_alert.critical | firing |
metric_alert.warning | firing |
metric_alert.resolved | resolved |
issue.created | firing |
issue.unresolved | firing |
issue.resolved | resolved |
issue.ignored | resolved |
issue.archived | resolved |
Deduplication keys#
Issue-based alerts:
sentry:issue:<issue_id>
Metric alerts:
sentry:metric:<sentry_alert_id>
Fallback:
make_dedup_key("sentry", external_id, title, labels)
Route create/update payload#
Creating a route without a secret is allowed so the user can get the webhook URL first:
{
"team_id": 1,
"name": "Sentry Backend",
"source": "sentry",
"rotation_id": null,
"escalation_policy_id": null,
"channel_ids": [],
"matchers": {},
"group_by": ["project_slug", "issue_id"],
"integration_config": {},
"enabled": true
}
Saving the secret later:
{
"team_id": 1,
"name": "Sentry Backend",
"source": "sentry",
"rotation_id": null,
"escalation_policy_id": null,
"channel_ids": [],
"matchers": {},
"group_by": ["project_slug", "issue_id"],
"integration_config": {
"sentry": {
"webhook_secret": "client-secret-from-sentry"
}
},
"enabled": true
}
On update, an empty Sentry secret means: keep the existing saved secret.
Switching a route from source=sentry to another source should clear integration_config.