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 |
|---|---|
|
|
Atlassian account ID of the user who created the approval |
|
|
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 |
|---|---|
|
|
Identifier of the step that was decided |
|
|
Type of the step: |
|
|
The decision made: |
|
|
Atlassian account ID, email address, or |
|
|
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 |
|---|---|
|
|
Either |
Common Fields
Every webhook payload includes these fields:
|
Field |
Description |
|---|---|
|
|
Unique identifier for this event |
|
|
ISO 8601 UTC timestamp of when the event occurred |
|
|
One of: |
|
|
Your Atlassian site URL |
|
|
|
|
|
Approval identifier |
|
|
Name of the approval |
|
|
Jira issue ID or Confluence page ID |
|
|
Jira project ID or Confluence space ID |
|
|
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 |
|---|---|
|
|
Successful delivery |
|
|
Your endpoint returned an error HTTP status |
|
|
Could not establish a connection within 5 seconds |
|
|
Your endpoint did not respond within 10 seconds |
|
|
A network-level error occurred during delivery |
|
|
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 |
|---|---|
|
|
|
|
|
Base64-encoded ECDSA signature of the request body |
|
|
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
-
Read the raw request body bytes
-
Base64-decode the
Signatureheader -
Fetch the public key using the
Signature-Key-Timestampheader value -
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 endpointconst 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.