Webhook Reference
Receive real-time HTTP POST notifications when portfolio events occur. Webhooks are available on Gold plans and above, and all payloads are signed with HMAC-SHA256.
Last reviewed
Overview
How webhooks work in the Portfolio Tracker.
Webhooks allow your applications to receive real-time HTTP POST notifications whenever key events occur in your portfolio. Instead of polling the API for changes, you register an endpoint URL and the Portfolio Tracker sends event data to it automatically.
- Webhooks are available on Gold plans and above. Professional plans have their own webhook entitlement and a different set of events
- All payloads are delivered as JSON via HTTP POST
- Every delivery is signed with HMAC-SHA256 so you can verify authenticity
- You subscribe by ticking the event types you want; there is no wildcard subscription, so a new event type needs adding to an existing endpoint
Tip
Event Types
Each webhook delivery includes an event type that identifies what happened.
These are the event types you can subscribe to on a consumer plan. Every delivery is a flat JSON object carrying the event name, the time it was raised, and a data object holding the affected record. You can also send yourself a test delivery from the endpoint list at any time. It arrives with the event type webhook.test and a fixed message body, so you can confirm your handler and signature check work before a real event fires.
transaction.created
Fired when a new transaction (buy, sell, dividend or transfer) is recorded in your portfolio. Transactions created by a statement import are not sent individually. They are covered by import.completed instead, so a single upload cannot flood your endpoint with hundreds of deliveries.
{
"event": "transaction.created",
"timestamp": "2026-04-03T10:15:00.123456+00:00",
"data": {
"id": "6f1c0a2e-5d4b-4a91-9f7c-2b8e3d4a5c60",
"type": "buy",
"symbol": "AAPL",
"quantity": "10.0000000000",
"price_per_unit": "185.5000000000",
"currency": "USD",
"transaction_date": "2026-04-03",
"account_id": "1d9b6c44-0f2a-4e7b-8c31-77a5e9d0b412"
}
}corporate_action.detected
Fired when a stock split, dividend distribution or merger has been applied to holdings you actually hold. The counters report what it changed.
{
"event": "corporate_action.detected",
"timestamp": "2026-04-03T14:30:00.123456+00:00",
"data": {
"id": "9c2f7b18-3e5a-4d62-b0f4-6a1c8e2d9f37",
"action_type": "split",
"symbol": "NVDA",
"ex_date": "2026-06-10",
"lots_modified": 3,
"transactions_created": 1
}
}price_alert.triggered
Fired when a watchlist price alert condition is met, for example when a security’s price crosses a threshold you set. The direction field tells you which way the threshold was breached.
{
"event": "price_alert.triggered",
"timestamp": "2026-04-03T09:45:00.123456+00:00",
"data": {
"id": "4b7d1e90-8c26-4f53-a1d8-95e0c3b7a642",
"watchlist_id": "2a6e5f31-7b40-49c8-8d17-0e4b9a3c5d28",
"symbol": "TSLA",
"direction": "above",
"threshold": "250.0000000000",
"current_price": "251.3000000000",
"currency": "USD"
}
}tax.recalculated
Fired when a tax year is recalculated for you, such as after a new transaction or corporate action adjusts your cost basis. One delivery is sent per tax year recalculated.
{
"event": "tax.recalculated",
"timestamp": "2026-04-03T11:00:00.123456+00:00",
"data": {
"id": "8e3a5c72-1f94-4b06-9d28-c7b1f0e6a534",
"jurisdiction": "GBR",
"tax_year": 2026,
"calculation_method": "section_104",
"net_taxable_gains": "1234.5600000000",
"currency": "GBP",
"calculated_at": "2026-04-03T10:59:58.654321+00:00"
}
}import.completed
Fired when a brokerage statement import reaches a terminal state, whether it succeeded (including with partial errors) or failed. Read the status field to tell the two apart.
{
"event": "import.completed",
"timestamp": "2026-04-03T12:20:00.123456+00:00",
"data": {
"id": "0c58d3a6-9e21-4f7b-8a40-b6d2e1c9f503",
"broker_name": "Interactive Brokers",
"status": "completed",
"rows_parsed": 50,
"transactions_created": 47,
"transactions_skipped": 2,
"transactions_failed": 1
}
}Delivery Headers
Headers included with every webhook delivery.
Each webhook HTTP POST request includes the following headers to help you identify, verify, and process the delivery:
- X-Webhook-Signature: HMAC-SHA256 hex digest of the raw request body, computed using your endpoint secret
- X-Webhook-Event: The event type string (e.g.
transaction.created) - X-Webhook-ID: A unique UUID for this delivery attempt
- X-Webhook-Timestamp: Unix timestamp (seconds) of when this delivery attempt was sent
- webhook-id: A
msg_-prefixed event identifier that stays the same on every retry of the same event (Standard Webhooks) - webhook-timestamp: The same Unix timestamp as
X-Webhook-Timestamp(Standard Webhooks) - webhook-signature:
v1,followed by a base64 HMAC-SHA256 signature over the id, timestamp and body (Standard Webhooks) - User-Agent:
TheWill-Portfolio-Webhooks/1.0 - Content-Type:
application/json
Tip
X-Webhook-ID, including retries of the same event, so treat it as an attempt identifier. Use webhook-id, which repeats on retries, to recognise an event you have already processed, and make your handler idempotent.Signature Verification
How to verify that a webhook delivery is authentic.
Every webhook delivery is signed so you can confirm it originated fromOrchard72 and has not been tampered with. To verify a delivery:
- Read the raw request body as bytes (do not parse it first)
- Compute the HMAC-SHA256 digest of the raw body using your endpoint secret as the key
- Compare your computed hex digest with the value in the
X-Webhook-Signatureheader - If they match, the delivery is authentic. If not, reject the request
Python example
import hmac
import hashlib
def verify_signature(payload_body, secret, signature_header):
expected = hmac.new(
secret.encode("utf-8"),
payload_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)JavaScript example
const crypto = require("crypto");
function verifySignature(payloadBody, secret, signatureHeader) {
const expected = crypto
.createHmac("sha256", secret)
.update(payloadBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}Standard Webhooks verification
Deliveries also follow the Standard Webhooks specification, so an off-the-shelf Standard Webhooks library can verify them. That signature covers the timestamp too, so you can reject old or replayed deliveries. Sign {webhook-id}.{webhook-timestamp}.{raw body} with HMAC-SHA256, using your endpoint secret’s UTF-8 bytes as the key, and compare the base64 result with the value after v1, in webhook-signature. A Standard Webhooks library expects the key base64-encoded, so pass it the base64 encoding of your endpoint secret.
import base64
import hmac
import hashlib
def verify_standard_webhook(payload_body, secret, headers):
signed = (
f"{headers['webhook-id']}.{headers['webhook-timestamp']}.".encode()
+ payload_body
)
expected = base64.b64encode(
hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
).decode()
return any(
hmac.compare_digest(f"v1,{expected}", candidate)
for candidate in headers["webhook-signature"].split(" ")
)Important
hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js) to prevent timing attacks.Retry Behaviour
What happens when a delivery fails.
Your endpoint must return a 2xx status code to acknowledge successful receipt of a webhook delivery. If the endpoint returns a non-2xx response or does not respond within 10 seconds, the delivery is retried with exponential backoff:
- Immediately (first attempt)
- After 1 minute
- After 5 minutes
- After 30 minutes
- After 2 hours
- Then once daily for up to 5 days
- These intervals are the earliest a retry is attempted. Retries are picked up by a periodic sweep, so the actual gap may be longer
Automatic deactivation
After 10 consecutive failed deliveries, the endpoint is automatically deactivated and you will receive an email notification. Reactivating a deactivated endpoint is not currently possible from the Developer page. You need to delete it and register it again, which issues a new signing secret.
Tip
200 response as quickly as possible. Process the webhook payload asynchronously (e.g. via a background job queue) to avoid timeouts.Managing Webhooks
How to create, update, and delete webhook endpoints.
You can manage your webhook endpoints from the Developer page or programmatically via the API.
- Open the Developer page at
/account/developerand find the Webhooks panel (the Portfolio Tracker Developer page at/assets/portfolio/developershows the same panel) - Enter your endpoint under “Endpoint URL (HTTPS)” and click “Register endpoint”. The URL must use HTTPS and must resolve to a publicly reachable address
- Tick the event types you want under “Events”. At least one is required
- Save the endpoint. Your signing secret will be displayed once, so copy it immediately
Each registered endpoint has its own row with History, Test, Rotate secret and delete controls. Changing an endpoint’s URL or subscribed events is currently possible through the API only. To delete an endpoint, click the delete icon and confirm. Rotating the signing secret is done from the Rotate secret button on the row, and the new secret is shown once.
Note
Related Topics
Can’t find what you’re looking for?
Our support team is here to help. Contact us and we’ll get back to you as soon as possible.
Contact Support
