Summary
Jira webhook routing is broken in two ways:
- Wrong lodash paths — uses
payload.webhookEvent and payload.user.accountId but Jira sends these at the root level (webhookEvent, user.accountId)
- Wrong connection matching strategy — matches on
user.accountId (the individual Jira user who performed the action), but this is fundamentally incorrect. Every other Nango integration matches on a workspace/org-level identifier.
This supersedes #5400, which correctly identified problem (1) but not (2). PR #5408 (the pending fix for #5400) only removes the payload. prefix, which would fix issue events but still fail for comment, sprint, and board events.
Root Cause
Problem 1: Incorrect paths
The handler passes payload.webhookEvent and payload.user.accountId to executeScriptForWebhooks, but Jira payloads have these at the root level. lodash.get(body, 'payload.user.accountId') returns undefined, so connection matching always fails.
Problem 2: Wrong identifier strategy
Even with corrected paths, matching on user.accountId is wrong because:
- It only matches if the authing user triggered the event. If any other user in the Jira workspace creates an issue or adds a comment, it won't match any connection.
user is not present on all event types. Comment events put the user at comment.author.accountId. Sprint and board events have no user field at all.
- Every other Nango integration matches on a workspace-level identifier:
| Provider |
Matches on |
| GitHub App |
installation_id |
| HubSpot |
portalId |
| Linear |
organizationId |
| Slack |
team.id |
| Shopify |
subdomain |
| Jira (current) |
accountId (individual user) |
Evidence
Real comment_created webhook payload from Nango logs — note webhookEvent is at root, and there is no root-level user field:
{
"webhookEvent": "comment_created",
"timestamp": 1773931953555,
"matchedWebhookIds": [208],
"issue": {
"self": "https://example.atlassian.net/rest/api/2/issue/42766",
...
},
"comment": {
"author": {
"accountId": "712020:a77f5b24-...",
...
},
"self": "https://example.atlassian.net/rest/api/2/issue/42766/comment/61142",
...
}
}
The response was 400, confirming routing failure.
Suggested Fix: Match on baseUrl via self URL
Every Jira webhook payload contains resource objects (issue, comment, sprint, board, etc.) with a self URL that includes the Jira site domain (e.g., https://example.atlassian.net/rest/api/2/...). The Jira post-connection script already stores baseUrl in connection_config.
The fix is to extract the site origin from any self URL in the payload and match it against baseUrl:
const route: WebhookHandler = async (nango, _headers, body) => {
// Extract base URL from any self link in the payload
const selfUrl =
body?.issue?.self ||
body?.comment?.self ||
body?.sprint?.self ||
body?.board?.self ||
body?.worklog?.self ||
body?.version?.self;
const baseUrl = selfUrl ? new URL(selfUrl).origin : undefined;
const response = await nango.executeScriptForWebhooks({
body,
webhookType: 'webhookEvent',
connectionIdentifierValue: baseUrl,
propName: 'baseUrl'
});
return Ok({
content: { status: 'success' },
statusCode: 200,
connectionIds: response?.connectionIds || [],
toForward: body
});
};
Why this approach
- Works for all event types —
self URLs are present on every Jira resource object (Atlassian docs: "webhook payloads are the same shape returned by the Jira REST API")
- Matches at workspace level — consistent with how every other integration works
- No external dependencies — doesn't require API-based webhook registration or 30-day refresh cycles
- Uses existing data —
baseUrl is already stored by the Jira post-connection script
Verified self presence by event type
| Event type |
self location |
Evidence |
jira:issue_created/updated |
issue.self |
Official docs |
comment_created/updated |
comment.self |
Server docs + REST API |
sprint_created/started/closed |
sprint.self |
Captured payloads |
board_created/updated |
board.self |
REST API schema (no webhook example exists) |
worklog_created/updated |
worklog.self |
REST API |
version_created/updated |
version.self |
REST API |
Array handling
The current handler also supports array payloads. The same approach applies — extract self from each event in the array.
References
Summary
Jira webhook routing is broken in two ways:
payload.webhookEventandpayload.user.accountIdbut Jira sends these at the root level (webhookEvent,user.accountId)user.accountId(the individual Jira user who performed the action), but this is fundamentally incorrect. Every other Nango integration matches on a workspace/org-level identifier.This supersedes #5400, which correctly identified problem (1) but not (2). PR #5408 (the pending fix for #5400) only removes the
payload.prefix, which would fix issue events but still fail for comment, sprint, and board events.Root Cause
Problem 1: Incorrect paths
The handler passes
payload.webhookEventandpayload.user.accountIdtoexecuteScriptForWebhooks, but Jira payloads have these at the root level.lodash.get(body, 'payload.user.accountId')returnsundefined, so connection matching always fails.Problem 2: Wrong identifier strategy
Even with corrected paths, matching on
user.accountIdis wrong because:useris not present on all event types. Comment events put the user atcomment.author.accountId. Sprint and board events have nouserfield at all.installation_idportalIdorganizationIdteam.idsubdomainaccountId(individual user)Evidence
Real
comment_createdwebhook payload from Nango logs — notewebhookEventis at root, and there is no root-leveluserfield:{ "webhookEvent": "comment_created", "timestamp": 1773931953555, "matchedWebhookIds": [208], "issue": { "self": "https://example.atlassian.net/rest/api/2/issue/42766", ... }, "comment": { "author": { "accountId": "712020:a77f5b24-...", ... }, "self": "https://example.atlassian.net/rest/api/2/issue/42766/comment/61142", ... } }The response was 400, confirming routing failure.
Suggested Fix: Match on
baseUrlviaselfURLEvery Jira webhook payload contains resource objects (
issue,comment,sprint,board, etc.) with aselfURL that includes the Jira site domain (e.g.,https://example.atlassian.net/rest/api/2/...). The Jira post-connection script already storesbaseUrlinconnection_config.The fix is to extract the site origin from any
selfURL in the payload and match it againstbaseUrl:Why this approach
selfURLs are present on every Jira resource object (Atlassian docs: "webhook payloads are the same shape returned by the Jira REST API")baseUrlis already stored by the Jira post-connection scriptVerified
selfpresence by event typeselflocationjira:issue_created/updatedissue.selfcomment_created/updatedcomment.selfsprint_created/started/closedsprint.selfboard_created/updatedboard.selfworklog_created/updatedworklog.selfversion_created/updatedversion.selfArray handling
The current handler also supports array payloads. The same approach applies — extract
selffrom each event in the array.References