Add support for approval webhooks

Description

We need to add the ability to listen for approval-related events via webhooks (creation, step decision, completion).

This will be done by allowing administrators to configure Webhook URLs in Global Settings, to which notifications will be sent when specific approval events occur.

The overall design and security mechanism should be consistent with the implementation already used in Contract Signatures.

Scope

1. Webhook Configuration

  • Add a Global Settings section for configuring one or more Webhook URLs

  • Configured URLs will receive HTTP notifications for approval-related events


2. Supported Webhook Events

Webhooks should be triggered for the following events:

  • Approval created

  • Approval step decided (approve / reject / abstain, if applicable)

  • Approval completed

    • final approved

    • final rejected

Each webhook payload should include sufficient context to identify:

  • approval

  • step (if applicable)

  • decision

  • timestamps


3. Webhook Security / Verification

  • Expose an endpoint that allows consumers to retrieve a public key along with a timestamp

  • This key can be used by webhook receivers to verify that the webhook was sent by our system

  • The signing / verification mechanism should follow the same approach as implemented in Contract Signatures

Prepare docs for webhooks and link them on UI webhook settings page.

Webhooks can be tested with: https://webhook.site/

DOCS:

Webhooks

Overview

Webhooks allow you to receive real-time HTTP notifications when approval events occur, eliminating the need to poll the API for changes. When an event is triggered, Approval Path sends a POST request with a JSON payload to your configured URL.

You can configure separate webhook URLs for each event type in Settings > Webhooks.

Configuration

Navigate to Settings > Webhooks > Settings tab to configure your webhook endpoints. Each event type has its own URL field:

  • Approval creation webhook — triggered when a new approval is created

  • Step decision webhook — triggered when a step receives a decision

  • Approval completion webhook — triggered when an approval reaches its final outcome

All webhook URLs must use HTTPS. To disable a webhook, clear its URL field and save. Webhook configuration requires global admin privileges.

Event Types

creation

Fired when a new approval is created.

{  "eventUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",  "eventTimestamp": "2026-02-26T14:00:00.123Z",  "eventType": "creation",  "hostUrl": "https://yoursite.atlassian.net",  "product": "jira",  "approvalId": "1057",  "approvalName": "Budget Approval",  "referenceId": "10003",  "collectionId": "10000",  "definitionId": "5",  "creatorId": "5b10a2844c20165700ede21g",  "stepsCount": 3}

Field

Description

creatorId

Atlassian account ID of the user who created the approval

stepsCount

Number of approval steps in the path

step-decision

Fired when a step in the approval path receives a decision.

{  "eventUuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901",  "eventTimestamp": "2026-02-26T14:05:00.456Z",  "eventType": "step-decision",  "hostUrl": "https://yoursite.atlassian.net",  "product": "jira",  "approvalId": "1057",  "approvalName": "Budget Approval",  "referenceId": "10003",  "collectionId": "10000",  "definitionId": "5",  "stepId": "a3f7c1d2-8e4b-4f9a-b123-0d1e2f3a4b5c",  "stepType": "user",  "decision": "accepted",  "decidedBy": "5b10a2844c20165700ede21g",  "comment": "Looks good"}

Field

Description

stepId

Identifier of the step that was decided

stepType

Type of the step: user, group, vote, email, automation, http

decision

The decision made: accepted, rejected, abstained, or voted

decidedBy

Atlassian account ID, email address, or "unknown" for automated steps

comment

Approver's comment. Only present when provided

completion

Fired when an approval process reaches its final outcome.

{  "eventUuid": "c3d4e5f6-a7b8-9012-cdef-123456789012",  "eventTimestamp": "2026-02-26T14:10:00.789Z",  "eventType": "completion",  "hostUrl": "https://yoursite.atlassian.net",  "product": "jira",  "approvalId": "1057",  "approvalName": "Budget Approval",  "referenceId": "10003",  "collectionId": "10003",  "definitionId": "5",  "outcome": "approved"}

Field

Description

outcome

Either "approved" or "rejected"

Common Fields

Every webhook payload includes these fields:

Field

Description

eventUuid

Unique identifier for this event

eventTimestamp

ISO 8601 UTC timestamp of when the event occurred

eventType

One of: creation, step-decision, completion

hostUrl

Your Atlassian site URL

product

jira or confluence

approvalId

Approval identifier

approvalName

Name of the approval

referenceId

Jira issue ID or Confluence page ID

collectionId

Jira project ID or Confluence space ID

definitionId

Approval path definition identifier

Empty or null fields are omitted from the payload.

Delivery

  • Webhooks are sent asynchronously and do not block the approval operation

  • Each webhook is delivered once — there are no automatic retries

  • HTTP redirects are followed automatically

  • Your endpoint must respond within 10 seconds (read timeout)

  • A 5-second connection timeout applies

  • Responses with HTTP 2xx status codes are considered successful

  • Delivery failures (non-2xx responses, timeouts, connection errors) are logged and visible in the Call History tab

Call History

The Call History tab in Settings > Webhooks shows a log of all webhook deliveries. Use it to monitor delivery status and troubleshoot failures.

Filtering

You can filter the call history by:

  • Event Type — Creation, Step Decision, Completion

  • Status — Success or Error

  • Approval — search by approval name

  • Date — filter by date range

Viewing Error Details

For failed deliveries, expand the row to see:

  • URL — the endpoint that was called

  • Error — the error message or response body returned by your endpoint (truncated to 1000 characters)

Status Codes

Status

Description

2xx

Successful delivery

4xx, 5xx

Your endpoint returned an error HTTP status

Connect timeout

Could not establish a connection within 5 seconds

Read timeout

Your endpoint did not respond within 10 seconds

IO error

A network-level error occurred during delivery

Error

An unexpected error occurred

Retention

Only the last 30 days of call history are stored. Older entries are automatically removed daily.

Signature Verification

Every webhook request includes a cryptographic signature so you can verify it originated from Approval Path and was not tampered with.

Headers

Header

Description

Content-Type

application/json

Signature

Base64-encoded ECDSA signature of the request body

Signature-Key-Timestamp

ISO 8601 UTC timestamp identifying which signing key was used

Algorithm

  • Curve: secp384r1 (NIST P-384)

  • Signature algorithm: SHA384withECDSA

  • Public key format: X.509 DER-encoded

Fetching the Public Key

Retrieve the public key by calling:

GET {baseUrl}/hosts/{hostId}/webhooks-signing-public-key.der?timestamp={Signature-Key-Timestamp}

Pass the exact value from the Signature-Key-Timestamp header as the timestamp query parameter. The public key URL (with your host ID) is available on the webhook settings page.

Verification Steps

  1. Read the raw request body bytes

  2. Base64-decode the Signature header

  3. Fetch the public key using the Signature-Key-Timestamp header value

  4. Verify using SHA384withECDSA

Example (Java)

byte[] requestBody = // raw request body bytesbyte[] signature = Base64.getDecoder().decode(signatureHeader); byte[] publicKeyDer = // fetched from the public key endpointKeyFactory keyFactory = KeyFactory.getInstance("EC");PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyDer)); Signature verifier = Signature.getInstance("SHA384withECDSA");verifier.initVerify(publicKey);verifier.update(requestBody);boolean valid = verifier.verify(signature);

Example (Node.js)

const crypto = require('crypto'); const requestBody = // raw request body as Bufferconst signature = Buffer.from(signatureHeader, 'base64');const publicKeyDer = // fetched from the public key endpoint const publicKey = crypto.createPublicKey({    key: publicKeyDer,    type: 'spki',    format: 'der'}); const valid = crypto.verify(    'SHA384',    requestBody,    { key: publicKey, dsaEncoding: 'der' },    signature);

Key Rotation

Signing keys are rotated automatically every 91 days. Always use the Signature-Key-Timestamp header from each webhook request to fetch the correct public key — do not hardcode or cache keys indefinitely.