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
POSTrequests. 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=ops3. 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.
| Field | Meaning |
|---|---|
id | The alert's ID. It stays the same on every delivery attempt. |
type | Always budget.threshold.alarm. |
text | A one-line summary, ready to post to chat. |
threshold | The threshold that crossed, with its optional message. |
time_period | The budget window the alert is about. |
amount | The budget's amount for the window. |
target | The threshold's level as an amount. |
observed | The spending, or for a forecasted threshold the forecast, that crossed the level. |
as_of | When 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-monthlyEach 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 opsThe new secret replaces the old one immediately, so update your endpoint at the same time.
Next steps
- Read how notification channels deliver alerts.
- Look up the channel operations in the API reference.