API Reference
Complete REST API documentation for IPCraft. Manage folders, subnets, IP addresses, VLANs, VRFs, locations, webhooks, and your team programmatically.
Base URL
All endpoint paths below are relative to this base URL. All resource IDs are UUIDs.
Authentication
All requests except the Network Tools endpoints require authentication via one of two methods:
- Session cookie — set by the magic-link and Google OAuth flows. HttpOnly, Secure, SameSite=Lax, 7-day sliding expiry.
- API key — pass an
Authorization: Bearer ipc_...header. Create keys in Settings or via the API Keys endpoints.
X-Requested-With: XMLHttpRequest header for CSRF protection. API key requests do not need this header. Session requests that the browser marks as started by another website (Sec-Fetch-Site: cross-site), such as a link on a third-party page to an export URL, are refused with 403; API keys are unaffected.
API keys carry read and/or write permissions (both by default). Two additional restrictions apply to write requests:
- Free plan: API keys are read-only. POST/PATCH/DELETE requests with an API key return
403regardless of the key's permissions. - Viewer role: users with the viewer role cannot make write requests (
403). - Session-only actions: inviting or changing members, billing, and creating or deleting API keys require a signed-in browser session. POST/PATCH/DELETE requests to the Team, Billing, and API Keys endpoints with an API key return
403; the GET endpoints in those sections still work with a key. A key carries its owner's role, so this keeps a leaked key from inviting a new admin or minting fresh keys that would outlive its revocation.
Every response carries an X-API-Version: v1 header.
curl https://api.ipcraft.io/api/v1/folders \
-H "Authorization: Bearer ipc_abc123..."
Example (Session cookie with CSRF)
curl -X POST https://api.ipcraft.io/api/v1/folders \
-H "Content-Type: application/json" \
-H "X-Requested-With: XMLHttpRequest" \
-b "__Host-ipcraft_session=..." \
-d '{"name": "Production"}'
Errors & Plan Limits
All errors return a JSON object with an error key and an appropriate HTTP status code.
{
"error": "subnet overlaps with existing subnet 10.0.0.0/16"
}
| Status | Meaning |
|---|---|
400 | Bad request — invalid input or validation error |
401 | Unauthorized — missing, invalid or expired credentials (an expired API key returns the error API key expired) |
402 | Payment required — plan limit exceeded |
403 | Forbidden — insufficient permissions, CSRF failure, or free-plan API write |
404 | Not found |
409 | Conflict — the change clashes with existing data (a duplicate network or name, an IP recorded twice, a gateway inside a child subnet, an exhausted IP pool, the last admin leaving), or a PATCH lost a race: the subnet or address was changed by another request between this one reading and writing it. Nothing was written; send the same PATCH again |
413 | Request body too large — over 1 MB, or 5 MB for a scan and 10 MB for an import |
429 | Rate limited — too many requests; wait for the Retry-After header |
502 | Bad gateway — a webhook test ping got no 2xx answer, or an invitation email could not be sent |
503 | Temporarily unavailable — a shared capacity limit is reached; retry after the Retry-After header |
Plan Limits
| Limit | Free | Pro ($29/mo) | Teams ($199/mo) | Enterprise |
|---|---|---|---|---|
| Users | 1 | 2 | 10 | Custom |
| Folders | 5 | Unlimited* | Unlimited* | Custom |
| Subnets | 100 | Unlimited* | Unlimited* | Custom |
| IP Addresses | 1,500 | 5,000 | 10,000 | Custom |
| VLANs | 10 | Unlimited* | Unlimited* | Custom |
| VRFs | 5 | Unlimited* | Unlimited* | Custom |
| Locations | 5 | Unlimited* | Unlimited* | Custom |
| Webhooks | — | 2 | 50 | 500 |
| API Keys | 5 | 100 | 100 | Custom |
| API Access | Read-only | Read/Write | Read/Write | Read/Write |
| Rate Limit (per user) | 10 req/s | 50 req/s | 50 req/s | Custom |
| Burst (per user, per second) | 20 | 100 | 100 | Custom |
| Activity Log | 7 days | 30 days | 90 days | Custom |
* Unlimited features are subject to the fair use limits described in the Terms of Service.
Requests that would exceed your plan limits return 402. Authenticated requests are rate-limited per user: a budget of 60 × your plan’s rate per minute, and a burst cap of twice the rate (at least 5) per second. Every response carries X-RateLimit-Limit (the per-minute budget), X-RateLimit-Remaining, X-RateLimit-Reset (Unix time the minute window ends) and X-RateLimit-Policy (600;w=60, 20;w=1 on Free). A 429 carries Retry-After in seconds: 1 after a burst rejection, 60 when the minute budget is spent. Back off by Retry-After, not X-RateLimit-Reset; a burst rejection also uses one request of the minute budget.
Failed authentication is limited per client IP address (IPv6 clients per /64, with a whole /48 allowed four times that), with API keys and session cookies counted separately: after 30 requests in a minute whose API key was rejected with 401, requests from that address that carry an unfamiliar API key return 429 with Retry-After: 60 without checking it, and the same holds for session cookies. A key or session that authenticated in the last hour, or a session that has just signed in, is still accepted, so one client failing with a revoked key doesn’t lock out others on the same address. Requests that send no credential at all are not counted.
Bulk Limits
Every bulk request handles at most 1,000 items, on every plan. A request over the limit returns 400 (for example "maximum 1,000 addresses per request") and changes nothing. Send larger jobs as several requests of 1,000 or fewer; the web app already does this, sending bigger selections and CSV files in batches of 1,000.
| Request | Limit per request |
|---|---|
| Batch Create addresses | 1,000 IPs |
| Batch Update addresses | 1,000 IDs |
| Batch Delete addresses | 1,000 IDs |
| Import Subnets, Import Addresses | 1,000 rows (10 MB body) |
| Scan reconcile apply | 1,000 hosts written: records marked seen plus records created (5 MB paste) |
Each bulk request writes one activity entry, and that entry lists every record the request touched (ips or networks). Batch create also lists the IPs it skipped because they were already recorded, under skipped_ips. These lists are never truncated, in the activity log or in webhook payloads. Plan limits still apply to every item, so a request within 1,000 items can still be refused with 402. Split takes a part count instead of a list (a power of two up to 1,024) and is not a bulk request in this sense.
Auth Endpoints
IPCraft is passwordless. Sign in with a magic link or Google OAuth — there are no register or password endpoints. Accounts and organizations are created automatically on first sign-in. Send Magic Link and Google OAuth are limited to 5 requests per minute per IP, and the verify and accept-invite pages to 20; Send Magic Link also has per-email and daily limits (below).
Send Magic Link
Send a passwordless sign-in link to the given email address. Each link works once and expires after 15 minutes. Requesting another link doesn't cancel the earlier ones; signing in with any of them cancels the rest. Limited to 3 links per email address per 15 minutes. Past that the endpoint returns 429, and the links already sent still work. Each IP can request 200 links per day. Once 60 links have gone out from an IP in a day, a link to an address that doesn’t have an account yet gets 429 with a Retry-After header, and an address that has one still gets its link with the same answer. Sign-in email to new addresses is also limited service-wide per hour; past that limit it returns 503 with a Retry-After header, and an address that already has an account still gets its link. A higher service-wide hourly ceiling covers all sign-in email; past it the endpoint returns 503 and sends nothing. Each of these answers is the same for every address. Requests must carry an X-Requested-With header (any value) and, if they send an Origin, come from the app; anything else, such as a form posted from another website, gets 403.
{
"email": "john@example.com"
}
Response
{
"message": "sign-in link sent to john@example.com"
}
Verify Magic Link
Verify the magic link token from the email. This endpoint is opened by the browser, not called programmatically. The GET shows a confirmation page, and its button posts the token back to the same URL, so mail scanners that prefetch links don't use it up. The POST consumes the token, sets the __Host-ipcraft_session cookie and redirects (303) to the app. On first sign-in it creates the user and their organization. Invalid or expired tokens redirect to the login page with an error. A POST submitted from another website is refused with a 403 page, and the link in the email keeps working.
Google OAuth
Start the Google OAuth sign-in flow. Redirects to Google; the callback (/api/v1/auth/oauth/google/callback) sets the session cookie and redirects to the app. Browser-only — not for programmatic use.
Get Current User
Returns the authenticated user, their organization, and their role (admin, editor, or viewer). Requires auth.
{
"user": {
"id": "7f3e9a12-4b6c-4d2e-9f01-8a5b3c7d1e42",
"email": "john@example.com",
"name": "John Smith",
"email_verified": true,
"created_at": "2026-03-01T12:00:00Z"
},
"organization": {
"id": "c2a8f5d0-1e3b-4a7c-8d9e-0f1a2b3c4d5e",
"name": "John Smith's Org",
"slug": "john-smiths-org",
"plan": "free",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:00Z"
},
"role": "admin"
}
Logout
Destroys the current session and clears the session cookie. No request body required.
Response200 OK
Delete Account
Permanently deletes the authenticated user's account and clears the session. If the user is the only member of their organization, the organization and all of its data are deleted with it (subnets, addresses, VLANs, VRFs, folders, locations, webhooks, API keys and the activity log); this cannot be undone. Returns 409 if the user is the only admin of an organization with other members (promote someone first), and 403 with an API key: deleting an account needs a signed-in browser session.
{
"message": "account deleted"
}
Folders
Folders are top-level organizational containers for grouping subnets (e.g., "Production", "Development").
List Folders
Returns all folders for the authenticated organization.
Response[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Production",
"description": "Production network ranges",
"sort_order": 0,
"created_at": "2026-03-01T12:00:00Z"
}
]
Create Folder
Create a new folder.
Request body{
"name": "Production",
"description": "Production network ranges",
"sort_order": 0
}
Only name is required. description and sort_order are optional.
Returns 201 with the created folder object.
Get Folder
Returns a single folder by ID.
Update Folder
Update a folder. All fields are optional — only provided fields are changed.
Request body{
"name": "Prod (US-East)",
"description": "US East production ranges",
"sort_order": 1
}
Response
200 OK
Delete Folder
Delete a folder. Subnets in the folder are kept and become unfoldered.
Response204 No Content
Subnets
Subnets represent CIDR network ranges. They form a hierarchy — creating a subnet within an existing range automatically nests it as a child. Overlap detection prevents conflicting ranges within the same VRF.
List Subnets
List all subnets, optionally filtered by folder. Includes utilization statistics and resolved VLAN/VRF/location/folder names. Not paginated.
Query parametersfolder_id— filter by folder (optional)vlan_id,vrf_id,location_id— filter by VLAN, VRF or location (optional)
[
{
"id": "b4c5d6e7-f8a9-0123-bcde-f01234567890",
"folder_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"network": "10.0.0.0/16",
"name": "Core network",
"description": "Main production range",
"vlan_id": null,
"vrf_id": null,
"location_id": null,
"is_pool": false,
"gateway": "10.0.0.1",
"dns_primary": "10.0.0.53",
"dns_secondary": null,
"scan_enabled": false,
"tracked": true,
"utilization": 0.12,
"used_count": 7860,
"total_count": 65534,
"folder_name": "Production",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:00Z"
}
]
Nullable/empty fields (parent_id, vlan_number, vrf_name, location_name, custom_fields, counts) are omitted when unset.
Create Subnet
Create a new subnet. The parent is auto-detected based on CIDR containment (and existing subnets inside the new range are re-parented under it). Address records the parent already holds inside the new range move into the new subnet, except any that would be its network or broadcast address. If the parent's gateway moves with them, the gateway field moves too: the parent's is cleared, and the new subnet takes it over unless the request sets a different gateway. The same CIDR can't be created twice in one VRF (or twice outside any VRF); that returns 409.
{
"network": "10.0.1.0/24",
"name": "Web servers",
"description": "Web tier subnet",
"folder_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"vlan_id": null,
"vrf_id": null,
"location_id": null,
"gateway": "10.0.1.1",
"dns_primary": "10.0.0.53",
"dns_secondary": null,
"is_pool": false,
"tracked": true
}
Only network is required. All other fields are optional. network must be the network address itself: 10.0.1.5/24 returns 400 naming 10.0.1.0/24. An IPv4-mapped IPv6 prefix such as ::ffff:10.0.1.0/120 is stored as the IPv4 prefix it denotes, 10.0.1.0/24. tracked defaults to true when omitted; send false to create a structure-only container subnet that's excluded from utilization stats. is_pool marks an IP pool, which lets Create Address pick the next free IP when the request has none.
Returns 201 with the created subnet object. If the new subnet (created without a VRF) overlaps ranges that exist in other VRFs, the response is wrapped as {"subnet": {...}, "warnings": [{"id", "network", "name", "vrf_name"}]} — the create still succeeds.
Get Subnet
Returns a subnet with utilization statistics and its child subnets.
Response{
"id": "b4c5d6e7-f8a9-0123-bcde-f01234567890",
"network": "10.0.0.0/16",
"name": "Core network",
"utilization": 0.12,
"children": [
{
"id": "c5d6e7f8-a9b0-1234-cdef-012345678901",
"network": "10.0.1.0/24",
"name": "Web servers"
}
]
}
Update Subnet
Update subnet metadata. All fields are optional — only provided fields are changed. The network itself cannot be changed.
{
"name": "Web servers (prod)",
"description": "Production web tier",
"folder_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"location_id": null,
"vlan": "120",
"vrf": "CORP",
"is_pool": true,
"tracked": false,
"gateway": "10.0.1.1",
"dns_primary": "10.0.0.53",
"dns_secondary": null,
"custom_fields": { "rack": "R12" }
}
Note: unlike create, update takes vlan as a VLAN number (1–4094) and vrf as a VRF name; both are auto-created if they don't exist, and an empty string clears the assignment. vlan_id and vrf_id (UUIDs, as on create) are also accepted and win over the legacy fields.
Changing the VRF moves the subnet into that VRF's tree. Its child subnets stay in the old VRF and move up to its former parent. In the new VRF it nests under the most specific subnet that contains it, adopts the subnets and records inside its range as create does, and returns 409 if the same CIDR already exists there or if another subnet in the new VRF already records one of its IPs. Its own records that fall inside a subnet it now contains move to the most specific such subnet, and if its gateway would fall inside one, the move returns 409. The activity entry lists the moved records under moved_down.
A gateway inside one of the subnet's child subnets returns 409: a gateway belongs to the most specific subnet that contains it, so set it on the child. The same applies on create when the new subnet contains existing subnets.
200 OK
Delete Subnet
Delete a subnet and its own address records. Child subnets are kept: each direct child moves up one level, to the deleted subnet's parent (or to the top level), with its addresses intact. Webhook subscribers get subnet.deleted for the subnet and, when children moved, one subnet.updated listing them (see Webhooks).
204 No Content
Split Subnet
Split a subnet into equal parts. The number of parts must be a power of 2 (2, 4, 8, ... up to 1024). The original subnet is deleted and the parts are created in its place, with new IDs. Each part inherits the original's name, description, folder, parent, VLAN, VRF, location, DNS servers, custom fields, and pool, scan, and tracked flags. Webhook subscribers get subnet.deleted for the original and one subnet.created listing every part with its new ID (see Webhooks).
Address records move into the part that contains them, and the gateway goes to whichever part contains it (unless it would be that part's network or broadcast address). Existing child subnets are re-parented under the part that contains them. A part that falls inside a larger subnet already below the original nests under the narrowest such subnet instead of the original's parent, and takes over that subnet's address records and gateway inside its range, as a newly created child would. A part that would duplicate an existing child subnet returns 409, as does a split that would move two records of one IP into the same part, one from the original and one from the subnet the part nests under.
discard_addresses (default false) to true to permanently delete the original's address records (and clear its gateway) instead of moving them. Records held by subnets below the original are kept, and still move into a part that nests inside their subnet.{
"parts": 4,
"discard_addresses": false
}
Response
[
{ "id": "d6e7f8a9-...", "network": "10.0.0.0/26" },
{ "id": "e7f8a9b0-...", "network": "10.0.0.64/26" },
{ "id": "f8a9b0c1-...", "network": "10.0.0.128/26" },
{ "id": "a9b0c1d2-...", "network": "10.0.0.192/26" }
]
Subnet Usage
Returns utilization statistics for a subnet.
Response{
"subnet_id": "c5d6e7f8-a9b0-1234-cdef-012345678901",
"network": "10.0.1.0/24",
"used": 42,
"total": 254,
"utilization": 0.165
}
First Free IP
Returns the first available IP address in the subnet, or null if the subnet is full. The ranges of child subnets are skipped, as are IPs an enclosing subnet already records, so the IP returned can always be created here.
{
"ip": "10.0.1.5"
}
Response (full subnet)
{
"ip": null,
"message": "no available addresses"
}
Free Blocks
Finds unallocated child blocks of a requested size inside a subnet — the subnet-level counterpart of First Free IP. Returns the lowest free CIDR blocks of prefix length prefix that don't collide with any existing subnet in the same VRF (nested children are handled; supernets are ignored). prefix must be longer than the subnet's own prefix. limit caps the candidates returned (default 5, max 50); total is the count of all free blocks, and capped is true when the total hit the reporting cap of 1,000,000 (large IPv6 containers).
{
"subnet_id": "c5d6e7f8-a9b0-1234-cdef-012345678901",
"network": "10.0.0.0/16",
"prefix": 24,
"blocks": ["10.0.4.0/24", "10.0.5.0/24"],
"total": 253,
"capped": false
}
List Overlaps
Returns all pairs of subnets whose CIDR ranges overlap, classified by overlap type: duplicate (same CIDR in different VRFs), containment (parent/child), or partial.
[
{
"subnet_a": { "id": "...", "network": "10.0.0.0/16", "vrf_name": "CORP" },
"subnet_b": { "id": "...", "network": "10.0.0.0/16" },
"type": "duplicate"
}
]
Addresses
Individual IP address records within a subnet. Each address is validated against the parent subnet's CIDR range. Valid status values are assigned, reserved, d-dhcp (dynamic DHCP), m-dhcp (manual DHCP), and gateway.
An address is recorded on the most specific subnet that contains it, within its VRF, so each IP has one record. A subnet refuses an IP that falls inside one of its child subnets (at any depth) with 409, naming the child: "10.77.0.5 is inside child subnet 10.77.0.0/26; record it there". A child's own network and broadcast addresses are refused on the parent the same way. A subnet also refuses, with 409, an IP that an enclosing subnet already records, which happens when the record was made before the child existed; release that record first. Every path that creates records follows this rule: create, batch create, CSV import, scan reconcile, and the gateway field.
List Addresses
List IP addresses in a subnet. Paginated: limit defaults to 1,000 (also the maximum); the total count is returned in the X-Total-Count response header.
[
{
"id": "0a1b2c3d-4e5f-6789-0abc-def123456789",
"subnet_id": "c5d6e7f8-a9b0-1234-cdef-012345678901",
"ip": "10.0.1.10",
"hostname": "web-01.prod",
"mac_address": "00:1a:2b:3c:4d:5e",
"description": "Primary web server",
"status": "assigned",
"owner": "platform-team",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:00Z"
}
]
Optional fields (ptr_record, custom_fields, last_seen) are omitted when unset.
Create Address
Assign an IP address within a subnet. The IP must fall inside the subnet's CIDR range and not already be assigned. An IP inside a child subnet, or one an enclosing subnet already records, returns 409 (see Addresses).
{
"ip": "10.0.1.10",
"hostname": "web-01.prod",
"mac_address": "00:1A:2B:3C:4D:5E",
"description": "Primary web server",
"status": "assigned",
"owner": "platform-team"
}
ip is the only required field, unless the subnet is an IP pool (below). status defaults to assigned.
Returns 201 with the created address object.
Auto-assign on an IP pool. On a subnet with is_pool set to true, leave ip out (or send it empty) and the API records the next free address, the same one First Free IP would return, and gives it back in the created object. The IP is picked inside the create, so concurrent requests each get a different one; calling first-free and then creating can hand two callers the same IP. A pool with no free address left returns 409 ("no free addresses left in 10.0.1.0/24"). On a subnet without is_pool, or a container subnet, a missing ip is a 400. Batch create doesn't auto-assign.
{
"hostname": "build-agent-07",
"owner": "ci"
}
Response (201)
{
"id": "7f8e9d0c-1b2a-3c4d-5e6f-708192a3b4c5",
"subnet_id": "c5d6e7f8-a9b0-1234-cdef-012345678901",
"ip": "10.0.1.11",
"hostname": "build-agent-07",
"status": "assigned",
"owner": "ci",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:00Z"
}
Batch Create
Assign up to 1,000 IPs in one request (e.g., reserve a range; see Bulk Limits). status applies to all IPs and defaults to reserved. IPs this subnet already records are skipped. An IP inside a child subnet, or one an enclosing subnet records, fails the whole batch with 409 and creates nothing.
{
"ips": ["10.0.1.20", "10.0.1.21", "10.0.1.22"],
"status": "reserved"
}
Response
{
"created": 3
}
Get Address
Returns a single address by ID (same shape as the list items above).
Update Address
Update address metadata. All fields are optional. The IP itself cannot be changed.
Request body{
"hostname": "web-01a.prod",
"mac_address": "00:1A:2B:3C:4D:5F",
"description": "Migrated web server",
"status": "assigned",
"owner": "platform-team",
"custom_fields": { "asset_tag": "A-1042" }
}
Response
200 OK
Delete Address
Remove an IP address assignment (release the IP).
Response204 No Content
Batch Update
Update the status and/or owner of up to 1,000 addresses at once (see Bulk Limits). IDs that match nothing in your organization are skipped; an ID that is not a UUID fails the request with 400.
{
"ids": ["0a1b2c3d-...", "1b2c3d4e-..."],
"status": "reserved",
"owner": "network-team"
}
Response
{
"updated": 2
}
Batch Delete
Release up to 1,000 addresses at once (see Bulk Limits). IDs that match nothing in your organization are skipped; an ID that is not a UUID fails the request with 400.
{
"ids": ["0a1b2c3d-...", "1b2c3d4e-..."]
}
Response
{
"deleted": 2
}
VLANs
Track VLAN assignments alongside your subnets. Standard CRUD:
A VLAN has a vlan_number (1–4094, required on create), optional name, description, and location_id. List responses include a computed subnet_count.
{
"id": "2c3d4e5f-6789-0abc-def1-234567890abc",
"vlan_number": 120,
"name": "Web tier",
"description": "Production web VLAN",
"subnet_count": 3,
"created_at": "2026-03-01T12:00:00Z"
}
VRFs
VRFs (virtual routing and forwarding instances) let the same CIDR exist more than once — overlap detection is scoped per VRF. Standard CRUD:
A VRF has a name (required on create), optional rd (route distinguisher, e.g. 65000:1) and description. List responses include a computed subnet_count. Deleting a VRF keeps its subnets and moves them out of any VRF, where they nest into the existing tree; webhook subscribers get one subnet.updated listing every subnet that moved or got a new parent. The delete returns 409 if one of them would duplicate a network that already exists outside any VRF, or if one of them and a subnet outside any VRF that it would nest with both record the same IP, which after the merge would be one address with two records. The message names the first conflict. After the merge, records that sit on a subnet but inside a subnet nested under it move to the most specific nested subnet (listed under moved_down in the activity entry); a gateway that would end up inside a nested subnet returns 409.
{
"id": "3d4e5f67-890a-bcde-f123-4567890abcde",
"name": "CORP",
"rd": "65000:1",
"subnet_count": 12,
"created_at": "2026-03-01T12:00:00Z"
}
Locations
Physical sites (offices, datacenters) that subnets and VLANs can be tagged with. Standard CRUD:
A location has a name (required on create) and optional address. List responses include computed subnet_count and vlan_count.
{
"id": "4e5f6789-0abc-def1-2345-67890abcdef1",
"name": "Datacenter US-East",
"address": "Ashburn, VA",
"subnet_count": 8,
"vlan_count": 4,
"created_at": "2026-03-01T12:00:00Z"
}
Webhooks
Subscribe to real-time event notifications. See the Webhooks guide for setup instructions and signature verification.
List Webhooks
List all webhook subscriptions. The signing secret is never included in list responses. For viewer members and API keys without write access (including every key on the Free plan), url is cut to its scheme and host, such as https://hooks.slack.com/…, and "url_masked": true is added, because a chat webhook’s URL lets anyone post to the channel. The same applies to Get Webhook, and List Deliveries omits response_body for those callers. The activity log and its CSV export show those callers webhook URLs in the same masked form.
[
{
"id": "5f678901-abcd-ef12-3456-7890abcdef12",
"url": "https://example.com/webhooks/ipcraft",
"description": "CMDB sync",
"event_types": ["subnet.created", "subnet.deleted"],
"is_active": true,
"failure_count": 0,
"dropped_count": 0,
"created_at": "2026-03-21T16:00:00Z"
}
]
An inactive webhook includes deactivated_reason: plan (paused by a plan downgrade; it turns back on when the plan has room for it), failures (disabled after 10 consecutive delivery failures), or user (disabled through Settings or this API). Active webhooks omit it, as do webhooks disabled before the field was added. dropped_count counts events that were never queued for the webhook because the organization already had 20,000 deliveries waiting for a retry (see Webhooks).
Create Webhook
Create a new webhook subscription. The URL must use HTTPS and be 2,048 characters or fewer; the description is limited to 500. Returns the signing secret — store it securely, it is only shown once.
Request body{
"url": "https://example.com/webhooks/ipcraft",
"description": "CMDB sync",
"event_types": ["subnet.created", "subnet.deleted"]
}
Leave event_types empty ([]) to receive all event types.
{
"id": "5f678901-abcd-ef12-3456-7890abcdef12",
"url": "https://example.com/webhooks/ipcraft",
"secret": "whsec_a1b2c3d4e5f6...",
"description": "CMDB sync",
"event_types": ["subnet.created", "subnet.deleted"],
"is_active": true
}
secret value immediately. It cannot be retrieved again. Use it to verify webhook signatures.Get Webhook
Get a single webhook subscription. The signing secret is not included.
Update Webhook
Update a webhook's URL, description, event types, or active status.
Request body{
"url": "https://new-endpoint.example.com/hook",
"event_types": ["address.created"],
"is_active": true
}
Response
200 OK
Delete Webhook
Permanently delete a webhook subscription and all its delivery history.
Response204 No Content
List Deliveries
List recent deliveries for a webhook. Each record is one delivery: attempt counts how many times it has been sent, and id is the delivery_id in the body and the X-Webhook-Delivery-ID header every attempt carried. Supports ?limit= (default 50, max 100) and ?offset= query parameters. Delivery records are retained for 7 days; one still waiting for a retry at 7 days is marked expired and removed a day later.
[
{
"id": "6789012a-bcde-f123-4567-890abcdef123",
"event_type": "subnet.created",
"response_status": 200,
"duration_ms": 142,
"attempt": 1,
"delivered_at": "2026-03-21T16:30:00Z"
}
]
Test Ping
Send a test ping event to the webhook endpoint. Returns the delivery result so you can verify connectivity.
{
"id": "789012ab-cdef-1234-5678-90abcdef1234",
"event_type": "ping",
"response_status": 200,
"duration_ms": 95,
"attempt": 1
}
Returns 502 if the endpoint is unreachable or returns anything but a 2xx, the same rule real deliveries use. Redirects are not followed, so a 3xx answer is a 502 whose error says so. Returns 402 on the free plan, 429 when the organization’s delivery budget of 100 per minute is spent, and 409 when the webhook’s signing secret can’t be read because the server’s encryption key changed; rotate the secret to fix it.
Rotate Secret
Generate a new signing secret for the webhook. The old secret stops working immediately.
Response{
"secret": "whsec_new_secret_value..."
}
API Keys
Manage API keys for programmatic access. The full key is only shown once at creation time. Creating and deleting keys requires a signed-in browser session: requests with an API key return 403. Listing keys works with either.
List Keys
List all API keys. Only the key prefix is shown — the full key is never returned after creation. expires_at is present on keys created with a lifetime; a key past it is listed until revoked but no longer authenticates.
[
{
"id": "789012ab-cdef-1234-5678-90abcdef1234",
"name": "Ansible automation",
"key_prefix": "ipc_a1b2c3d4",
"permissions": ["read", "write"],
"created_at": "2026-03-01T12:00:00Z",
"last_used_at": "2026-03-14T09:15:00Z",
"expires_at": "2026-05-30T12:00:00Z"
}
]
Create Key
Generate a new API key. The full key is only returned in this response — store it securely.
Request body{
"name": "Ansible automation",
"permissions": ["read", "write"],
"expires_in_days": 90
}
permissions is optional and defaults to ["read", "write"]. Use ["read"] for a read-only key. expires_in_days is optional: 1–365 gives the key a lifetime, after which requests with it return 401 with the error API key expired, and the key’s creator is emailed two weeks before (for lifetimes over two weeks); omitted (or 0) the key never expires. Any other value returns 400. A key cannot be extended; generate a new one and revoke the old one.
{
"id": "789012ab-cdef-1234-5678-90abcdef1234",
"name": "Ansible automation",
"key": "ipc_a1b2c3d4e5f6...(68 chars total)",
"key_prefix": "ipc_a1b2c3d4",
"permissions": ["read", "write"],
"created_at": "2026-03-01T12:00:00Z",
"expires_at": "2026-05-30T12:00:00Z"
}
expires_at is null for a key without a lifetime.
key value immediately. It cannot be retrieved again.Delete Key
Revoke an API key. Any requests using this key will immediately start returning 401.
204 No Content
Team
Manage organization members and invitations (any plan with more than one seat: Pro, Teams and Enterprise). Roles are admin, editor, and viewer. Inviting, changing, and removing members requires a signed-in browser session: requests with an API key return 403. Listing members and invitations works with either.
List Members
[
{
"user_id": "7f3e9a12-4b6c-4d2e-9f01-8a5b3c7d1e42",
"email": "john@example.com",
"name": "John Smith",
"role": "admin",
"accepted_at": "2026-03-01T12:00:00Z"
}
]
Update Member Role
{
"role": "editor"
}
Response
{
"message": "role updated"
}
Remove Member
Remove a member from the organization. Their API keys are revoked and the pending invitations they sent are cancelled in the same step; each cancelled invitation gets its own entry in the activity log.
Response{
"message": "member removed"
}
Invite User
Email an invitation to join your organization. Returns 402 if you're at your plan's seat limit. Invitation creation is limited to 10 per organization per hour. The invitee accepts via the emailed link (GET /api/v1/auth/accept-invite?token=..., browser-only).
{
"email": "teammate@example.com",
"role": "editor"
}
Response
Returns 201 with the invitation object (id, email, role, expires_at, created_at). If the invitation email can’t be sent, returns 502 and no invitation is created, so the request can simply be retried.
List Invitations
List pending invitations.
Cancel Invitation
{
"message": "invitation cancelled"
}
Billing
Manage your subscription plan via Stripe integration. Checkout, portal, and org-name endpoints require the admin role and a signed-in browser session: requests with an API key return 403. Get Plan works with either.
Get Plan
Returns current plan details, usage, and limits. usage.users counts members plus pending invitations and usage.webhooks active webhooks; limits adds users, webhooks, rate_per_sec and activity_retention_days to the resource caps, a limit of 100,000 or more means unlimited, and plans gives the same limits (plus api_write) for every plan. checkout_available_at is the moment a newly created organization may start its first paid checkout (see Checkout); it is null once that wait has passed. has_subscription is true while the organization still has a Stripe subscription, which on the Free plan means an unpaid one: pay it in the Customer Portal to restore the plan.
{
"plan": "free",
"org_name": "John Smith's Org",
"has_billing": false,
"has_subscription": false,
"checkout_available_at": "2026-09-06T22:32:05Z",
"checkout_cooldown_seconds": 600,
"usage": {
"folders": 1,
"subnets": 3,
"addresses": 42
},
"limits": {
"folders": 5,
"subnets": 100,
"addresses": 1500
}
}
Checkout
Creates a Stripe Checkout session for upgrading to Pro or Teams. Redirect the user to the returned URL. Returns 400 if the organization is already on a paid plan, or still has an unpaid subscription — use the Customer Portal to switch plans or pay instead. For Enterprise, contact us.
Organizations younger than the checkout cooldown (10 minutes by default) receive 403 with "code": "checkout_cooldown", retry_after_seconds, an RFC 3339 available_at, and a Retry-After header. The short wait exists to deter card testing; nothing else is restricted during it.
{
"plan": "pro"
}
plan is "pro" or "teams". Omitting the body defaults to "pro".
{
"url": "https://checkout.stripe.com/c/pay/cs_live_..."
}
Customer Portal
Creates a Stripe Customer Portal session for managing billing, invoices, and cancellation.
Response{
"url": "https://billing.stripe.com/p/session/..."
}
Update Organization Name
Rename your organization (max 100 characters). A name containing control characters (such as a line break) or invisible formatting characters (such as a right-to-left override or a zero-width space) returns 400. The zero-width joiner and non-joiner, which emoji and scripts such as Persian need, are allowed.
{
"name": "Acme Networks"
}
Response
200 OK
Import & Export
Bulk import and CSV export. See the CSV Import & Export guide for the UI workflow, column aliases, and normalization rules. Import request bodies are limited to 10 MB. Text fields are stored as sent, apart from control characters and escape sequences, which become spaces (descriptions keep their line breaks), and the one leading apostrophe the CSV export adds to values a spreadsheet would treat as a formula, which is removed so exported values can be posted back unchanged. The export adds a second apostrophe to a value that already starts with one in front of such a value, so the round trip always gives back the stored text. Folder, VRF and location names a subnet row creates are limited to 255 characters, as on their create endpoints.
Import Subnets
Create up to 1,000 subnets per request (see Bulk Limits). Folders, VLANs, VRFs, and locations referenced by name/number are auto-created. Duplicates (same CIDR + VRF) and partial overlaps are skipped, not errored. Rows that would exceed plan limits are reported as errors; successfully imported rows are kept (no rollback).
Request body{
"subnets": [
{
"network": "10.0.1.0/24",
"name": "Web servers",
"folder": "Production",
"vlan": "120",
"vrf": "CORP",
"location": "Datacenter US-East",
"gateway": "10.0.1.1",
"is_pool": false
}
]
}
Only network is required per row. Also accepted: description, dns_primary, dns_secondary, scan_enabled, tracked (defaults to true when absent), custom_fields.
{
"results": [
{ "row": 1, "status": "created", "id": "...", "network": "10.0.1.0/24" }
],
"summary": { "created": 1, "skipped": 0, "errors": 0 }
}
Import Addresses
Create up to 1,000 addresses per request (see Bulk Limits). Each row needs ip and subnet (the parent CIDR, which must already exist). Also accepted: vrf, hostname, mac_address, description, status, owner, ptr_record, custom_fields. Already-assigned IPs are skipped. A row whose IP lies inside a child subnet of its subnet, or is already recorded by an enclosing subnet, is reported as an error with the reason and the rest of the file still imports. Same response shape as subnet import.
vrf names the VRF that holds subnet, matched case-insensitively; a VRF without that subnet is a row error. When it is blank or absent, the row targets the subnet with that CIDR outside any VRF, or the only VRF copy if there is no global one. A CIDR held by several VRFs and no global subnet needs vrf, or the row is an error.
{
"addresses": [
{
"ip": "10.0.1.10",
"subnet": "10.0.1.0/24",
"vrf": "CORP",
"hostname": "web-01.prod",
"status": "assigned"
}
]
}
Export Subnets
Download all subnets as CSV (columns: network, name, description, folder, vlan, vrf, location, gateway, dns_primary, dns_secondary, is_pool, scan_enabled, tracked, custom_fields; custom_fields is a JSON object, blank when unset). Folder/VLAN/VRF/location are exported as human-readable names. folder_id optionally limits the export to one folder.
Export Addresses
Download addresses as CSV (columns: ip, subnet, vrf, hostname, mac_address, description, status, owner, ptr_record, custom_fields). vrf is the parent subnet's VRF name, blank outside a VRF. subnet_id optionally limits the export to one subnet; omit it to export all addresses. Rows are in IP order. If the export fails partway through, the connection is closed without the end of the response, so clients report a failed transfer rather than saving a short file.
Search
Full-text search across subnets and addresses.
Subnets match on network CIDR, name, or description. Addresses match on IP address, hostname, description, MAC address, owner, PTR record, or custom field value. Words are matched by prefix, and a MAC matches in colon, hyphen, Cisco dotted, or bare-hex notation, whole or by its leading octets. Returns matching subnets and addresses, up to 50 of each.
Query parametersq— search query (required, max 500 characters)
{
"subnets": [
{
"id": "c5d6e7f8-a9b0-1234-cdef-012345678901",
"network": "10.0.1.0/24",
"name": "Web servers"
}
],
"addresses": [
{
"id": "0a1b2c3d-4e5f-6789-0abc-def123456789",
"ip": "10.0.1.10",
"hostname": "web-01.prod"
}
]
}
Activity
Audit log of actions performed within your organization. History is retained per your plan (7/30/90 days; see Plan Limits).
Returns a paginated list of activity entries.
Query parameterslimit— entries to return (default 50, max 500)offset— pagination offsetaction,resource_type,user_id,since,until— optional filters
{
"entries": [
{
"id": 1042,
"user_id": "7f3e9a12-4b6c-4d2e-9f01-8a5b3c7d1e42",
"action": "create",
"resource_type": "subnet",
"resource_id": "c5d6e7f8-a9b0-1234-cdef-012345678901",
"changes": { "network": "10.0.1.0/24" },
"created_at": "2026-03-14T10:30:00Z"
}
],
"total": 137,
"users": { "7f3e9a12-...": "john@example.com" }
}
Activity Filters
Returns the distinct actions and resource_types present in your activity log, for building filter UIs.
Activity Export
Download the activity log as CSV, every entry that matches. Accepts the same filter parameters as the list endpoint. As with the address export, a failure partway through closes the connection instead of ending the file early.
Verify Activity Integrity
Recompute the activity log’s hash chain and report whether every entry still matches (see Concepts). A large log is checked a few seconds at a time: while the walk is still running the response has "complete": false and the counts so far, and the next request continues where it stopped. Repeat the request until complete is true. Only one request per organization runs at a time (another gets 409), an organization can start 12 new checks an hour (429 past that), and when the service is busy with other checks the request returns 503 with Retry-After.
{
"valid": true,
"entries_checked": 4821,
"unhashed_entries": 0,
"anchor_id": 10233,
"complete": true
}
anchor_id is the oldest entry, which starts the chain and is not itself checked. When an entry fails, valid is false and first_invalid_id names the first one.
Network Tools
Public endpoints for common network lookups. No authentication required. Rate-limited per IP address (IPv6 clients per /64, with a whole /48 allowed four times that): 10 requests per minute for lookups, and 3 requests per minute for active probes (ping, port scan, traceroute). Exceeding a limit returns 429 with a Retry-After header. When too many probes or connection checks (SSL, HTTP/2, headers) are already running service-wide, a request waits up to 3 seconds for a slot and then returns 503 with Retry-After. All tools together also count against a quota of 500 requests per client IP per 24 hours (an IPv6 /64, with the /48 allowed 2,000); past it the API returns 429 with Retry-After set to the seconds until the window resets.
Lookups that fail upstream (an unreachable host, a nameserver that doesn’t answer) return 200 with an error field; 4xx is reserved for invalid input and 429 for rate limits. Browser requests must come from ipcraft.io or app.ipcraft.io; cross-site embeds are refused with 403. Command-line and server-side clients are unaffected.
DNS Lookup
Resolve a domain against 3 independent resolvers: Cloudflare (1.1.1.1), Google (8.8.8.8), and Quad9 (9.9.9.9).
Query parametersdomain— domain name to resolve (required); underscore labels such as_dmarc.example.comorselector._domainkey.example.comare acceptedtype— record type: A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, TXT, CAA, DNSKEY, DS, NAPTR, TLSA, HTTPS, or SVCB (default: A). Other types, including AXFR, IXFR, and ANY, return400all=true— query all common record types at onceauthoritative=true— query the domain's own nameservers instead
{
"domain": "example.com",
"type": "A",
"results": [
{
"resolver": "1.1.1.1:53",
"records": ["93.184.216.34"],
"ttl": 300,
"response_time_ms": 2.3
},
{ "resolver": "8.8.8.8:53", "records": ["93.184.216.34"], "ttl": 300, "response_time_ms": 5.4 },
{ "resolver": "9.9.9.9:53", "records": ["93.184.216.34"], "ttl": 37, "response_time_ms": 0.7 }
]
}
DNS Propagation
Check how a DNS record has propagated across 8 resolvers around the world (Cloudflare, Google, Quad9, OpenDNS, Yandex, KT, Neustar, 114DNS). Each result includes the resolver's label and location in addition to the fields returned by DNS Lookup. Accepts the same record types as DNS Lookup.
Reverse DNS
Perform a reverse DNS (PTR) lookup for an IP address, with forward-confirmation (FCrDNS) of each returned name. At most the first 5 names are returned and checked; "truncated": true is added when the zone had more.
{
"ip": "8.8.8.8",
"arpa_query": "8.8.8.8.in-addr.arpa.",
"names": [
{ "name": "dns.google", "fcrdns_valid": true }
],
"authoritative_ns": "ns1.google.com"
}
WHOIS Lookup
Perform a WHOIS lookup for a domain or IP address. Returns the raw WHOIS text plus a parsed summary: for domains, registrar, created, expires, expires_in_days, updated, nameservers, status; for IPs, cidr, net_name, organization, abuse_contact.
{
"query": "example.com",
"raw": "Domain Name: EXAMPLE.COM\n...",
"parsed": {
"registrar": "IANA",
"created": "1995-08-14T04:00:00Z",
"expires": "2026-08-13T04:00:00Z",
"expires_in_days": 40,
"nameservers": ["a.iana-servers.net", "b.iana-servers.net"]
}
}
SSL Certificate Check
Inspect a host's TLS certificate chain: each certificate's subject, issuer, sans, validity window, and days_remaining, plus chain_valid, tls_version, and cipher_suite.
{
"host": "example.com",
"tls_version": "TLS 1.3",
"cipher_suite": "TLS_AES_128_GCM_SHA256",
"chain_valid": true,
"certificates": [
{
"subject": "example.com",
"issuer": "DigiCert TLS RSA SHA256 2020 CA1",
"sans": ["example.com", "*.example.com"],
"not_after": "2026-08-29T21:41:26Z",
"days_remaining": 57,
"is_expired": false
}
]
}
SPF Check
Fetch and validate a domain's SPF record: parsed mechanisms, dns_lookup_count against the 10-lookup limit, plus valid, warnings, and errors.
{
"domain": "example.com",
"found": true,
"valid": true,
"raw": "v=spf1 include:_spf.example.com ~all",
"mechanisms": [
{ "qualifier": "+", "mechanism": "include", "value": "_spf.example.com" }
],
"dns_lookup_count": 1,
"dns_lookup_limit": 10,
"warnings": ["Using ~all (softfail) instead of -all (hardfail)"]
}
DMARC Check
Fetch and validate a domain's DMARC record: policy, subdomain_policy, parsed tags, plus valid, warnings, and errors.
{
"domain": "example.com",
"found": true,
"valid": true,
"policy": "reject",
"raw": "v=DMARC1; p=reject; rua=mailto:...",
"tags": { "v": "DMARC1", "p": "reject" }
}
Blacklist Check
Check an IP against 7 DNS blocklists (Spamhaus ZEN, Barracuda, SpamCop, Mailspike, UCEPROTECT L1, PSBL, SpamRATS).
Response{
"ip": "203.0.113.42",
"listed_count": 0,
"total_checked": 7,
"results": [
{ "list": "SpamCop", "zone": "bl.spamcop.net", "listed": false }
]
}
ASN Lookup
Look up the ASN, announcing prefix, and registry data for an IP address or AS number.
Response{
"query": "8.8.8.8",
"type": "ip",
"asn": "15169",
"as_name": "GOOGLE - Google LLC, US",
"prefix": "8.8.8.0/24",
"country": "US",
"rir": "arin",
"allocated": "2023-12-28"
}
HTTP/2 Check
Check which HTTP protocol versions a host supports.
Response{
"host": "example.com",
"http11": true,
"http2": true,
"http3_advertised": false,
"alpn_negotiated": "h2",
"tls_version": "TLS 1.3"
}
Ping (TCP Check)
TCP connectivity check to a host. Probes ports 80 and 443 by default; pass ?port= to check a specific port. Rate-limited to 3 requests per minute.
{
"host": "example.com",
"ip": "93.184.216.34",
"results": [
{ "port": 80, "open": true, "latency_ms": 1.3 },
{ "port": 443, "open": true, "latency_ms": 1.0 }
],
"checked_at": "2026-03-21T16:30:00Z"
}
Port Scan
Scan 19 common ports on a host (FTP, SSH, HTTP, HTTPS, DNS, SMTP, databases, RDP, etc.), or pass ?ports= with a comma-separated list (max 25; repeated ports are scanned once). The host is resolved once (IPv4 preferred) and every port is probed on that address, returned as ip. Rate-limited to 3 requests per minute.
{
"host": "example.com",
"ip": "93.184.216.34",
"open_count": 2,
"total": 19,
"results": [
{ "port": 80, "service": "HTTP", "category": "Web", "open": true, "latency_ms": 2.0 },
{ "port": 443, "service": "HTTPS", "category": "Web", "open": true, "latency_ms": 1.7 },
{ "port": 22, "service": "SSH", "category": "Remote Access", "open": false }
]
}
Traceroute
Trace the network path to a host with 3 round-trip measurements per hop. Unresponsive hops have null values. Rate-limited to 3 requests per minute.
{
"host": "example.com",
"ip": "93.184.216.34",
"hops": [
{ "hop": 1, "ip": "172.31.1.1", "rtt1_ms": 2.3, "rtt2_ms": 2.7, "rtt3_ms": 2.8 },
{ "hop": 2, "ip": null, "rtt1_ms": null, "rtt2_ms": null, "rtt3_ms": null }
]
}
My IP
Returns the client's public IP address, plus country, datacenter (edge location), and hostname (reverse DNS) when available.
{
"ip": "203.0.113.42",
"country": "US",
"datacenter": "DEN"
}
Request Headers
Returns the HTTP headers sent by the client. Pass ?url=https://... to instead fetch a remote site and get back its response headers with a security_checklist (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy). The url must be https:// on port 443; its query string and fragment are dropped, and the URL actually fetched is returned as url. Requests identify themselves with User-Agent: IPCraft-Tools/1.0.
{
"headers": {
"User-Agent": "curl/8.1.0",
"Accept": "*/*"
},
"method": "GET",
"proto": "HTTP/1.1"
}