Webhooks
Receive real-time HTTP notifications when resources change in your IPCraft account.
Overview
Webhooks let you subscribe to events in IPCraft — like a subnet being created, an IP address being assigned, or a VLAN being deleted. When an event occurs, IPCraft sends an HTTP POST request to your endpoint with a JSON payload describing what happened.
Common use cases include syncing IPAM data with a CMDB, triggering automation workflows, sending Slack notifications, and keeping audit systems up to date.
Creating a Webhook
- Go to Settings in the IPCraft app
- Scroll to the Webhooks section
- Click Add Webhook
- Enter your endpoint URL (must be
https://) - Optionally add a description and select specific event types
- Click Create Webhook
Event Types
You can subscribe to specific event types or leave the selection empty to receive all events. Available event types:
| Event | Description |
|---|---|
subnet.created | A new subnet was added |
subnet.updated | A subnet was modified |
subnet.deleted | A subnet was removed |
address.created | An IP address was assigned |
address.updated | An IP address record was updated |
address.deleted | An IP address was released |
folder.created | A new folder was created |
folder.updated | A folder was renamed or modified |
folder.deleted | A folder was removed |
vlan.created | A new VLAN was added |
vlan.updated | A VLAN was modified |
vlan.deleted | A VLAN was removed |
vrf.created | A new VRF was added |
vrf.updated | A VRF was modified |
vrf.deleted | A VRF was removed |
Payload Format
Webhook payloads are JSON objects with the following structure:
{
"event": "subnet.created",
"delivery_id": "5f0c2e8a-3b1d-4c7e-9a64-2d8f1b0e7c35",
"resource_type": "subnet",
"resource_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-03-21T16:30:00Z",
"data": {
"network": "10.0.1.0/24",
"name": "Production Web Servers",
"label": "10.0.1.0/24",
"source": "web"
}
}
delivery_id identifies the delivery: every retry of it carries the same value, and it matches the X-Webhook-Delivery-ID header. Because it is inside the signed body, it is the value to deduplicate on.
The data field is always present. It holds the event’s resource fields (for an update, each changed field as {"old": …, "new": …}) plus three keys that describe the request rather than the resource: label (the resource’s display name), source (web or api) and, for requests made with an API key, api_key_id and api_key_name. Skip those when you treat data as a list of changed fields.
Batch create, update, and delete send one event per request, not one per address. Their data.ips lists every address the request touched and data.count gives the total. A batch create also lists the IPs it skipped as already recorded in data.skipped_ips. A batch update gives each changed field’s new value as data.status.new or data.owner.new, and lists every record’s prior state in data.records, each with its id, ip, subnet_id, and the old value of each changed field. data.subnet_id is present only when every address in the request is in one subnet. A request holds at most 1,000 addresses (the bulk limit), and these lists are never truncated.
A CSV import, and a scan apply that creates records for undocumented hosts, also send one event per request: subnet.created for a subnet import, address.created for an address import or a scan apply. data.created is the number of records created, and data.networks (subnets) or data.ips (addresses) lists every one of them. resource_id is the import batch’s ID, or the scanned subnet’s ID for a scan apply, not the ID of an individual record. A scan apply that matches existing records also sends one address.updated for the subnet: data.ips lists every record it marked seen, data.filled gives each hostname or MAC address it filled in as {"ip", "hostname": {"old", "new"}, "mac_address": {"old", "new"}}, and data.via is scan.
A split deletes the original subnet and creates its parts under new IDs, so it sends two events. subnet.deleted is for the original, with data.split_into listing the parts’ networks. One subnet.created covers every part: its resource_id and data.split_from are the original’s ID, data.created is the number of parts, data.networks lists them, and data.subnets gives each part’s id, network, name, parent_id, vrf_id and folder_id.
Several changes give existing subnets a new parent or VRF besides the subnet they act on. Deleting a subnet moves its child subnets up to its parent, and deleting a VRF moves its subnets out of the VRF and re-nests them with the subnets outside any VRF. Creating or importing a subnet that contains existing ones makes them its children. Moving a subnet to another VRF leaves its children behind under its old parent and adopts the subnets it contains in the new VRF. A split puts the original’s children under the part that contains them. When any of these moves at least one subnet, one subnet.updated follows the request’s own event. Its resource_id is the ID of the subnet or VRF acted on (for an import, the import batch’s ID), data.via is subnet_delete, vrf_delete, subnet_create, subnet_import, subnet_vrf_move or subnet_split, data.count is the number of subnets moved, and data.subnets gives each one’s id, network and new parent_id and vrf_id, listing a subnet moved twice in one request once, in its final state. As with batch events, these lists are never truncated.
Verifying Signatures
Every webhook request includes an X-Webhook-Signature header for verifying authenticity. The signature uses HMAC-SHA256 with the signing secret you received when creating the webhook.
Signature format
The header value looks like:
t=1711046400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8f9
Where t is the Unix timestamp and v1 is the HMAC-SHA256 hex digest.
Verification steps
- Parse the
tandv1values from the header - Read the event type from the
X-Webhook-Eventheader - Construct the signed payload:
{timestamp}.{event_type}.{request_body}— the event type is included so theX-Webhook-Eventheader can't be tampered with - Compute HMAC-SHA256 of the signed payload using your webhook secret
- Compare your computed signature with the
v1value using a constant-time comparison - Optionally verify the timestamp is recent (within 5 minutes), and deduplicate on the body’s
delivery_id: together they stop a captured request from being replayed
Example: Python
import hmac, hashlib
def verify_webhook(payload: bytes, sig_header: str,
event_type: str, secret: str) -> bool:
# sig_header is X-Webhook-Signature,
# event_type is X-Webhook-Event
parts = dict(p.split("=", 1) for p in sig_header.split(","))
timestamp = parts["t"]
expected = parts["v1"]
signed = f"{timestamp}.{event_type}.".encode() + payload
computed = hmac.new(
secret.encode(), signed, hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, expected)
Example: Node.js
const crypto = require('crypto');
function verifyWebhook(payload, sigHeader, eventType, secret) {
// sigHeader is X-Webhook-Signature,
// eventType is X-Webhook-Event
const parts = Object.fromEntries(
sigHeader.split(',').map(p => p.split('=', 2))
);
const signed = `${parts.t}.${eventType}.${payload}`;
const computed = crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed), Buffer.from(parts.v1)
);
}
Request Headers
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | IPCraft-Webhooks/1.0 |
X-Webhook-Event | The event type (e.g., subnet.created) |
X-Webhook-Signature | HMAC-SHA256 signature for verification |
X-Webhook-Subscription-ID | UUID of the webhook subscription |
X-Webhook-Delivery-ID | Stable UUID of this delivery, the same value as the body’s delivery_id. Every retry of it carries the same value. It matches the delivery’s id in List Deliveries. The header is not signed, so deduplicate on the body’s delivery_id after verifying the signature. |
Retries & Failure Handling
Only a 2xx response counts as delivered. If your endpoint returns any other status or the request times out (10 second limit), IPCraft will retry the delivery with exponential backoff. Redirects are not followed, so a 3xx (an http-to-https hop, a trailing-slash redirect) fails every delivery: register the final URL.
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 25 minutes |
| 4th retry (final) | 2 hours |
Delivery is at least once: if your endpoint handles an event but its response is lost or arrives after the 10 second limit, the retry sends it again. Deduplicate on delivery_id from the verified body, which stays the same across retries. The X-Webhook-Delivery-ID header carries the same value but is not covered by the signature.
After 5 failed attempts (the initial delivery plus 4 retries), the delivery is abandoned. If a webhook accumulates 10 consecutive failures, it is automatically disabled to prevent unnecessary load. You can re-enable it from Settings after fixing the issue. Deliveries that were waiting for a retry when it was disabled are paused rather than dropped, and go out again when you re-enable it (if they are less than six days old). Events that happen while it is disabled are not queued.
Each organization has at most three deliveries in flight at once, and up to 100 waiting in the delivery queue, so a slow endpoint delays your own events rather than other customers’. Deliveries that can’t be sent right away, because your organization is over its delivery rate or its share of the queue is full, wait for the retry worker and go out about a minute later. At most 20,000 deliveries can be waiting per organization. Past that, new events are not queued: each one is counted in the webhook’s dropped_count (shown in Settings and returned by GET /api/v1/webhooks) so you can see what was missed.
Delivery Log
Every webhook in Settings has an expandable delivery log showing recent deliveries with their HTTP status, response time, attempt count, and timestamp. Use this to debug integration issues. Delivery logs are retained for 7 days. A delivery still waiting for a retry after 7 days is marked expired, with the error expired: not delivered within 7 days, and removed a day later.
Secret Rotation
You can rotate a webhook's signing secret at any time by clicking Rotate Secret in the webhook's expanded view. The old secret stops working immediately, so update your endpoint before rotating.
Plan Limits
Webhooks are available on paid plans. The number of webhook subscriptions depends on your plan:
| Plan | Webhooks |
|---|---|
| Free | — |
| Pro | 2 |
| Teams | 50 |
| Enterprise | Custom |
If a renewal payment fails, your plan and webhooks keep working while Stripe retries the charge. If the subscription ends or payment is never collected, the organization moves to the Free plan and its active webhooks are paused. When you subscribe again, paused webhooks turn back on automatically, oldest first, up to the new plan's limit. Webhooks you disabled yourself, or that were disabled after repeated delivery failures, stay off. Events that happen while a webhook is paused are not delivered later.
See the API reference for the full webhook REST API documentation.