Webhooks: get notified when something happens on your account
Add an HTTPS endpoint, choose the events it gets, verify the Nextify-Signature header, and use the delivery log to see and resend what was sent.
A webhook is a request Nextify sends to a URL of yours when something happens on your account: an instance is ready, a volume is attached, a job failed, an invoice was paid. Your scripts react to the event instead of asking the API over and over whether anything has changed.
Each event is a JSON POST to your endpoint, signed with a secret only you and Nextify know, so your endpoint can tell a real event from a forged one.
1. Add an endpoint
- In the portal, open Account → Webhooks.
- Under Add an endpoint, enter your endpoint's URL. It must:
- start with
https://, - use port 443 or a port from 1024 to 65535,
- point to a public address on the internet (a private, loopback or reserved address is refused, also when a DNS name resolves to one),
- be at most 2048 characters long, with no user name or password in it.
- start with
- Add a description if you like, for example "Deploy bot".
- Choose the events: All events (every type, including types added later) or Choose events and tick the ones you want, grouped by resource.
- Click Add endpoint.
The page then shows the endpoint's signing secret, which starts with whsec_. Copy it now and keep it in your application's secret store: this is the only time it is shown. If you lose it, rotate the secret (section 6) and use the new one.
An account can have up to 10 endpoints. You can do all of this with the API too: POST /v1/webhooks with an API token (see the API documentation).
2. The events
| Type | When | data.object |
|---|---|---|
instance.created | An instance was created and is ready | instance |
instance.started, instance.stopped | An instance was powered on or off | instance |
instance.deleted | An instance was deleted | instance |
instance.disk_resized | An instance's root disk was grown | instance |
volume.created, volume.attached, volume.detached, volume.resized, volume.deleted | The volume change finished | volume |
snapshot.created, snapshot.restored, snapshot.deleted | The snapshot change finished | snapshot |
bucket.created, bucket.deleted | The bucket change finished | bucket |
ip.attached, ip.detached | An extra public IP was attached to or detached from an instance | public IP |
load_balancer.created, load_balancer.updated, load_balancer.deleted | A load balancer was created, changed or deleted | load balancer |
job.succeeded | Any background job finished | job |
job.failed | A background job failed for good, after its retries | job |
invoice.paid | An invoice of your account was paid | invoice |
webhook.test | You clicked Send test event | a short message |
webhook.test only goes to the endpoint you send it to, never to the others.
3. What your endpoint receives
POST https://hooks.example.com/nextify
Content-Type: application/json
User-Agent: Nextify-Webhooks/1.0 (+https://www.nextifynow.com/kb/webhooks)
Nextify-Event-Id: evt_4f1c9e0b7a2d4c8e9f0a1b2c3d4e5f60
Nextify-Event-Type: instance.created
Nextify-Delivery-Id: 81234
Nextify-Signature: t=1793564103,v1=5b9f0c...e41a
{"id": "evt_4f1c9e0b7a2d4c8e9f0a1b2c3d4e5f60", "type": "instance.created",
"created": "2026-11-01T20:15:03Z", "api_version": "v1", "account_id": "...", "livemode": true,
"data": {"object": {"id": "...", "name": "web-1", "status": "running", ...},
"job": {"id": "48213", "type": "instance.create", "status": "succeeded", "error": null}}}
data.objectis the resource as it was when the event happened, with the same field names you get from the API.data.jobis there for job events and events that a job caused.Nextify-Event-Id(the same asidin the body) stays the same when a delivery is retried. Keep the ids you have handled and skip a repeat, so a retried event is not processed twice.
4. Verify the signature
Check every request before you trust it. The Nextify-Signature header looks like t=1793564103,v1=5b9f0c...:
tis the time the request was signed, in Unix seconds.v1is the HMAC-SHA256, in hex, oft, a dot and the raw request body, made with your endpoint's secret as the key (the whole secret,whsec_included).
To verify:
- Read the request body as raw bytes. Do not parse and re-serialise the JSON first: the bytes would differ and the check would fail.
- Compute the HMAC-SHA256 of
<t>.<raw body>with your secret and compare it withv1using a constant-time comparison. - Reject the request if
tis more than 5 minutes old. This stops someone from replaying an old request. Retries are signed again with a fresht.
Node.js (Express):
const crypto = require("node:crypto");
function verifyNextify(rawBody, header, secret) {
const f = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
if (!/^\d+$/.test(f.t ?? "") || Date.now() / 1000 - Number(f.t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(f.t + ".").update(rawBody).digest();
const given = Buffer.from(f.v1 ?? "", "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
app.post("/nextify", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyNextify(req.body, req.get("Nextify-Signature") ?? "", process.env.NEXTIFY_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body); // skip event.id if you have handled it already
res.sendStatus(204);
});
Python:
import hashlib, hmac, time
def verify_nextify(raw_body: bytes, header: str, secret: str) -> bool:
f = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t = f.get("t", "")
if not t.isdigit() or time.time() - int(t) > 300:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, f.get("v1", ""))
In Flask, pass request.get_data() as raw_body; in Django, request.body.
5. Answer quickly, and what happens when you do not
- Answer with any
2xxstatus within 10 seconds. If handling the event takes longer, store it, answer, and do the work afterwards. - Any other status, a timeout, or a connection error counts as a failure. Redirects are not followed: a
3xxanswer is a failure too, so give Nextify the final URL. - A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours and 10 hours, then every 12 hours, until 3 days after the event. After that it is marked Failed. A test event is tried only once.
- If every delivery to an endpoint has failed for 5 days, Nextify disables the endpoint and the Webhooks page says so. Fix the receiver, then click Enable: deliveries resume.
6. Test, check the log, resend, rotate
- Send test event sends a
webhook.testevent to that endpoint right away (the endpoint must be enabled). Use it after adding an endpoint to check that your receiver answers and that your signature check passes. - Deliveries opens the endpoint's log: every event sent in the last 30 days with its status, event type, HTTP status, number of attempts and time. Click a row to see the request body that was sent and your endpoint's answer (the first 2,000 characters). The log refreshes by itself while it is open.
- Resend on a delivery that was delivered or has failed sends that event again at once, with the same event id and a fresh signature. Useful after you have fixed a bug in your receiver.
- Rotate secret creates a new signing secret and shows it once. The old secret stops working immediately, so update your receiver right after rotating.
- Disable stops deliveries to an endpoint without deleting it; Delete removes it, and deliveries still waiting for it are dropped.
Good to know
- Each endpoint has its own secret. Never put it in code or a Git repository; keep it with your other secrets.
- Events can arrive out of order, especially after retries. Use the
createdtime in the body, or read the resource from the API, when order matters. - A read-only API token can list endpoints and read the log through the API; adding, changing, testing and resending need a full-access token or the portal.