Skip to main content

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

You can register multiple webhook endpoints, each listening for different event types. This is useful for routing events to different services.

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

Each delivery attempt carries its own 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:

  1. Read the raw request body as bytes (do not parse it first)
  2. Compute the HMAC-SHA256 digest of the raw body using your endpoint secret as the key
  3. Compare your computed hex digest with the value in the X-Webhook-Signature header
  4. 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

Always use a constant-time comparison function (such as 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

Return a 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.

  1. Open the Developer page at /account/developer and find the Webhooks panel (the Portfolio Tracker Developer page at /assets/portfolio/developer shows the same panel)
  2. Enter your endpoint under “Endpoint URL (HTTPS)” and click “Register endpoint”. The URL must use HTTPS and must resolve to a publicly reachable address
  3. Tick the event types you want under “Events”. At least one is required
  4. 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

Deleting a webhook endpoint is immediate and cannot be undone. Any in-flight retries for that endpoint will be cancelled.

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

We use cookies to improve your experience. See our Cookie Policy (opens in a new tab) for details.