AssetPilot Connect webhook signatures and retries
AssetPilot Connect sends signed events from AssetPilot for Jira to configured webhook endpoints. Use the request headers and raw body to verify each delivery before processing it.
AssetPilot Connect is not on Atlassian Marketplace yet. It requires AssetPilot's app REST API, which is not in production. This page describes the release candidate.
Add a webhook
- Open Webhooks and select Add webhook.
- Enter a name and the URL of your receiver.
- Choose events: selected events or All events.
- Select Save. The signing secret appears once. Copy it into your receiver's configuration, then select I've copied it.
- Select Send test. A healthy receiver shows Test event delivered with the HTTP status and time.
Use ⋯ on a row to Edit, Rotate secret or Delete a webhook.
Events
asset.created, asset.updated, asset.deleted, asset.status_changed, asset.checked_out, asset.checked_in,
asset.issue_linked, asset.issue_unlinked, asset.imported, asset.merged, stock.adjusted,
reservation.changed, maintenance.changed and custody.changed. Subscribe to selected events or all events (*).
Request
Every delivery is an HTTPS POST with a JSON body and these headers:
| Header | Value |
|---|---|
X-AssetPilot-Event | The event type, for example asset.created |
X-AssetPilot-Delivery | A unique delivery id. Use it to ignore duplicates. |
X-AssetPilot-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your webhook secret> |
The body looks like this:
| Field | Contents |
|---|---|
id | Event id |
type | Event type, for example asset.checked_out |
occurredAt | ISO 8601 timestamp |
site | { url, cloudId } of your Jira site |
data | The AssetPilot event: asset id and tag, kind, the actor's Atlassian account id, a summary and the changes |
Verify the signature
Check every request against the raw body before parsing it. Reject the request if the signature does not match or t is more than 300 seconds away from your clock.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyAssetPilotSignature(secret, header, rawBody, now = Math.floor(Date.now() / 1000)) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(now - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`, 'utf8').digest('hex');
const given = Buffer.from(parts.v1 ?? '', 'hex');
const want = Buffer.from(expected, 'hex');
return given.length === want.length && timingSafeEqual(given, want);
}
Python
import hashlib
import hmac
import time
def verify_assetpilot_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
The secret is shown once when you create the webhook. You can rotate it at any time.
Frameworks often parse JSON before your handler runs. Read the body as raw bytes, for example with express.raw({ type: 'application/json' }), verify it, then parse.
Retries and redelivery
Connect retries a network error or any non-2xx response up to 6 times with backoff, over about 9 hours.
The last 500 deliveries appear in Activity. You can redeliver any of them.
Return a 2xx quickly and do slow work afterwards. Use X-AssetPilot-Delivery to ignore a delivery you already processed.
Rotate the secret
Select ⋯ → Rotate secret and confirm. The current secret stops working immediately, and the new secret is shown once. Update your receiver straight away.
Allowed URLs
Webhook URLs must use https and must not point to private, loopback or link-local addresses.
Review Data and privacy for payload data and delivery log retention.