VEXXHOSTDocumentation
Guides

Receive alerts with a webhook

Send Budgets alerts to your own HTTPS endpoint as signed JSON, verify each request, and handle retries.

This guide sends a budget's alerts to an HTTPS endpoint you run, such as a chat integration, or an on-call system, and shows how to check that each request really comes from Budgets.

Before you begin

  • An HTTPS URL reachable from the internet that accepts POST requests. Budgets does not follow redirects.
  • A budget to send alerts from. See Create a budget.

1. Create the webhook channel

openstack budget channel create ops --type webhook --url https://hooks.example.com/budget-alerts
+------------+---------------------------------------------------+
| Field      | Value                                             |
+------------+---------------------------------------------------+
| id         | 8b2d4f6a-1c3e-4a5b-9d7f-2e4c6a8b1d3f              |
| name       | ops                                               |
| project_id | a6f1c2d93b8e4f07b5d4e2c1a9f8e7d6                  |
| type       | webhook                                           |
| status     | confirmed                                         |
| url        | https://hooks.example.com/budget-alerts           |
| address    | None                                              |
| secret     | whsec_Q2l4b0VyQ2pVd1Rsb3hNcUpwWGZ6Y1RnS3ZOYkhhRWQ |
| created_by | 4c8e2f1a9b7d4e6f8a3c5b1d7e9f2a4c                  |
| created_at | 2026-10-07T14:05:43Z                              |
+------------+---------------------------------------------------+

Store the secret now. Budgets shows it only when you create the channel or rotate its secret, and your endpoint needs it to verify requests.

2. Send a budget's alerts to it

Add the channel to the thresholds that should use it. On the CLI, --threshold replaces all of a budget's thresholds, so list every one you want to keep:

openstack budget set compute-monthly \
  --threshold name=warn,value=80,channel=finance,channel=ops \
  --threshold name=overrun,value=100,basis=forecasted,channel=finance,channel=ops

3. Handle the request

Each alert arrives as a POST with a JSON body:

POST /budget-alerts HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: vexxhost-budget
X-Budget-Signature: t=1792854007,v1=4b7e0f2c9a1d6e3b8f5c2a7d4e1b9c6f3a8d5e2b7c4f1a9e6d3b0c8f5a2e7d4b
{
  "id": "5e9a3c1f-7b2d-4f6e-9c8a-1d3b5f7e9a2c",
  "type": "budget.threshold.alarm",
  "text": "Budget \"compute-monthly\": spend 812.40 has reached 800.00 USD (threshold \"warn\")",
  "budget_id": "c7e1a5b3-9d2f-4e8c-a6b4-1f3d5e7c9a2b",
  "budget_name": "compute-monthly",
  "project_id": "a6f1c2d93b8e4f07b5d4e2c1a9f8e7d6",
  "threshold": {
    "name": "warn",
    "basis": "actual",
    "comparison": "greater_than_or_equal",
    "message": "Ask in #infra before adding servers."
  },
  "time_period": {"start": "2026-10-01T00:00:00Z", "end": "2026-11-01T00:00:00Z"},
  "currency": "USD",
  "amount": "1000",
  "target": "800",
  "observed": "812.395",
  "as_of": "2026-10-24T14:00:00Z"
}

Budgets sends the body as compact JSON. It is formatted here for reading.

FieldMeaning
idThe alert's ID. It stays the same on every delivery attempt.
typeAlways budget.threshold.alarm.
textA one-line summary, ready to post to chat.
thresholdThe threshold that crossed, with its optional message.
time_periodThe budget window the alert is about.
amountThe budget's amount for the window.
targetThe threshold's level as an amount.
observedThe spending, or for a forecasted threshold the forecast, that crossed the level.
as_ofWhen the spending was measured.

Answer with any 2xx status within 10 seconds. Budgets retries any other answer, or no answer, with increasing delays for about four hours. Because a retry can repeat an alert you already handled, ignore an id you have seen before.

4. Verify the signature

Check every request before you act on it. X-Budget-Signature holds a Unix timestamp t and v1, the hex-encoded HMAC-SHA256 of the timestamp, a period and the raw request body, keyed with the channel's secret. Reject the request if the signature does not match, or if t is more than five minutes from your clock. Compute the signature over the body exactly as it arrived, before you parse it.

import hashlib
import hmac
import time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(","))
    timestamp, signature = parts.get("t", ""), parts.get("v1", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Verify

Ask Budgets which alerts it sent and whether your endpoint accepted them:

openstack budget notification list compute-monthly

Each alert is pending while Budgets is still trying, sent once your endpoint answered with a 2xx status, or failed with the last error once it stops trying.

Rotate the secret

Replace the secret if it might have leaked:

openstack budget channel secret rotate ops

The new secret replaces the old one immediately, so update your endpoint at the same time.

Next steps

On this page