Webhooks
Webhooks tell your systems when something happens in your NewHost account, such as a deployment finishing, a payment clearing or a domain being registered. NewHost sends an HTTPS POST with a JSON body to each endpoint subscribed to that event.
Adding an endpoint
- In the dashboard: Developers → Webhooks → Add endpoint, or via the API with
POST /v1/webhooks. - The URL must be public and use
https://. - Pick the events you want, or all of them. You get a signing secret (
whsec_…) to verify requests. - Use Send test event to deliver a
pingevent and see your endpoint's response.
Event types
| Event | When |
|---|---|
app.created | An application was created |
app.deleted | An application was deleted |
deployment.started | A deployment began running |
deployment.succeeded | A deployment finished successfully |
deployment.failed | A deployment failed |
domain.registered | A domain was registered |
database.created | A database was provisioned |
payment.succeeded | A payment was accepted by Netcash |
payment.failed | A payment was declined |
subscription.activated | A plan subscription became active or renewed |
subscription.cancelled | A subscription was cancelled |
ping | Test event sent from the dashboard. |
Request format
Headers
POST /hooks/newhost HTTP/1.1
Content-Type: application/json
User-Agent: NewHost-Webhooks/1.0
X-NewHost-Event: deployment.succeeded
X-NewHost-Delivery: clx8dlv0001
X-NewHost-Signature: t=1790000000,v1=5d41402abc4b2a76b9719d911017c592...Body
{
"id": "evt_3f9d2c1ab4e5f6a7b8c9d0e1",
"type": "deployment.succeeded",
"created": "2026-09-24T10:17:42.000Z",
"data": { "applicationId": "clx8app0001", "deploymentId": "clx8dep0001", "commitSha": "9f2c1ab" }
}id is the same for every endpoint that receives the event and across retries, so use it to ignore duplicates. X-NewHost-Delivery identifies one delivery attempt.
Verifying signatures
X-NewHost-Signature holds a Unix timestamp t and v1, the hex HMAC-SHA256 of t + "." + raw body keyed with your endpoint's secret. Recompute it over the raw request body (before JSON parsing), compare in constant time, and reject timestamps older than five minutes to stop replays.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/hooks/newhost", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-NewHost-Signature") ?? "";
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", process.env.NEWHOST_WEBHOOK_SECRET)
.update(`${t}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
const valid = v1 && expected.length === v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
if (!fresh || !valid) return res.status(400).send("bad signature");
const event = JSON.parse(req.body);
// ...handle event.type, using event.id to skip duplicates
res.sendStatus(200);
});PHP
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_NEWHOST_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', $sig['t'] . '.' . $body, getenv('NEWHOST_WEBHOOK_SECRET'));
if (abs(time() - (int) $sig['t']) > 300 || !hash_equals($expected, $sig['v1'] ?? '')) {
http_response_code(400);
exit('bad signature');
}
$event = json_decode($body, true);
http_response_code(200);Python (Flask)
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/hooks/newhost")
def newhost():
parts = dict(p.split("=", 1) for p in request.headers.get("X-NewHost-Signature", "").split(","))
body = request.get_data()
expected = hmac.new(os.environ["NEWHOST_WEBHOOK_SECRET"].encode(), f"{parts.get('t')}.".encode() + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts.get("t", 0))) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = request.get_json()
return "", 200Responding and retries
- Return any
2xxwithin 10 seconds. Do slow work after responding, e.g. on a queue. - Anything else (or a timeout) is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours: six attempts over about 15 hours.
- Redirects are not followed; point the endpoint at the final URL.
- After 50 failed deliveries in a row the endpoint is disabled. Fix it, then enable it again in the dashboard.
- The endpoint page shows recent deliveries with the response we received, and lets you resend any of them.
Events can arrive more than once and out of order. Make handlers idempotent, keyed on the event
id.