IP pools and VIP operations¶
Only with own prefixes (a dedicated prefix from us or BYOIP), on shared or dedicated edges, see Shared and Dedicated. Your range is set up as IP pools bound to your company (sites). You assign their addresses (VIPs) to your vServices, release, move, swap, block or rotate them to another prefix, and announce standby pools. Requests need API access.
Endpoints¶
| Method | Path | Key | Purpose |
|---|---|---|---|
GET |
/pools |
read | List your IP pools |
GET |
/pools/{scope} |
read | One IP pool, address by address |
GET |
/pools/{scope}/metrics |
read | Traffic summed over one of your IP pools |
POST |
/vip-operations |
write | Run VIP operations as one change |
GET |
/vip-operations/{op_id} |
read | Where an operation stands |
GET /pools
Returns: 200, list of PoolSummary
Answer 200
[
{
"scope": "P-A",
"name": "a dedicated",
"ipv4_range": "203.0.113.0/28",
"ipv6_range": "2001:db8:a::/64",
"customer_announce": false,
"announced": true,
"announce_changed_at": null,
"next_change_at": null,
"total": 16,
"assigned": 2,
"blocked": 0,
"reserved": 0,
"free": 14
}
]
GET /pools/{scope}
Returns: 200, Pool
Answer 200
{
"scope": "P-A",
"name": "a dedicated",
"ipv4_range": "203.0.113.0/28",
"ipv6_range": "2001:db8:a::/64",
"customer_announce": false,
"announced": true,
"announce_changed_at": null,
"next_change_at": null,
"total": 16,
"assigned": 2,
"blocked": 0,
"reserved": 0,
"free": 14,
"addresses": [
{
"address": "203.0.113.0",
"ipv6": "2001:db8:a::",
"state": "free",
"service": null,
"blocked": false,
"released_at": null
},
{
"address": "203.0.113.1",
"ipv6": "2001:db8:a::1",
"state": "assigned",
"service": "a.example.com",
"blocked": false,
"released_at": null
},
{
"address": "203.0.113.2",
"ipv6": "2001:db8:a::2",
"state": "assigned",
"service": "tcp.example.com",
"blocked": false,
"released_at": null
}
]
}
GET /pools/{scope}/metrics
| Parameter | In | Meaning |
|---|---|---|
metric |
query | Chart; one of traffic, response_codes, protection, pre_http_filter, l4_packets, l4_flows, traffic_by_service; default "traffic" |
range |
query | Time window; one of 5m, 15m, 1h, 6h, 24h, 7d; default "1h" |
Returns: 200, MetricChart or TrafficByService
Answer 200: traffic
{
"metric": "traffic",
"range": "1h",
"step": "15s",
"series": [
{
"label": "at_site",
"values": [
[
1792750389,
1.5
],
[
1792750404,
2.0
]
],
"dashed": true,
"direction": "in"
},
{
"label": "inbound",
"values": [
[
1792750389,
1.5
],
[
1792750404,
2.0
]
],
"direction": "in"
},
{
"label": "passed_frontend",
"values": [
[
1792750389,
1.5
],
[
1792750404,
2.0
]
],
"direction": "in"
}
],
"scope": {
"pool": "P-A",
"services": 2,
"has_http": true,
"has_l4": true,
"partial_services": []
}
}
POST /vip-operations
| Parameter | In | Meaning |
|---|---|---|
Idempotency-Key |
header | Makes a retry return the first result; up to 128 characters |
Body: VipOperations
Returns: 201, VipOperationRecord
Request: assign
{
"operations": [
{
"op": "assign",
"service": "a.example.com",
"pool": "P-A",
"count": 1
}
]
}
Request: block
{
"operations": [
{
"op": "block",
"addresses": [
"203.0.113.9"
]
}
]
}
Answer 201: assign
{
"id": "f69c6909151c",
"idempotency_key": null,
"at": "2026-10-23T10:13:25.336158+00:00",
"by": "key:TGloIsNcCavQ",
"services": [
"a.example.com"
],
"pools": [],
"results": [
{
"op": "assign",
"service": "a.example.com",
"addresses": [
"203.0.113.0"
]
}
],
"commit": "f3b4fa1574c766a5567fb442fee9989f5729792a",
"replayed": false
}
Answer 201: block
{
"id": "85b0a4fd6a65",
"idempotency_key": null,
"at": "2026-10-23T10:13:25.393986+00:00",
"by": "key:TGloIsNcCavQ",
"services": [],
"pools": [],
"results": [
{
"op": "block",
"blocked": [
"203.0.113.9"
]
}
],
"commit": "bbe37eb5bfcf90d2369090b89e5c6ebb0e7c3a13",
"replayed": false
}
GET /vip-operations/{op_id}
Returns: 200, VipOperationStatus
Answer 200
{
"id": "f69c6909151c",
"idempotency_key": null,
"at": "2026-10-23T10:13:25.336158+00:00",
"by": "key:TGloIsNcCavQ",
"services": [
"a.example.com"
],
"pools": [],
"results": [
{
"op": "assign",
"service": "a.example.com",
"addresses": [
"203.0.113.0"
]
}
],
"commit": "f3b4fa1574c766a5567fb442fee9989f5729792a",
"state": "pending",
"edges": []
}
Fields¶
Pools are read-only; every change is a POST /vip-operations. A vService shows its
addresses in network.
PoolSummary¶
| Field | Type | Notes |
|---|---|---|
scope |
string | Pool id, used in paths and operations |
name |
string | Display name |
ipv4_range |
string | IPv4 range of the pool |
ipv6_range |
string | IPv6 range of the pool |
total |
integer | IPv4 addresses in the range, first and last included |
assigned |
integer | Addresses on your vServices |
blocked |
integer | Addresses on your block list |
reserved |
integer | Addresses withheld by Zero Services |
free |
integer | Addresses neither assigned, reserved nor blocked |
customer_announce |
boolean | You switch the announcement |
announced |
boolean | The edges announce the prefixes now |
announce_changed_at |
string | may be null; When you last switched the announcement; null if never |
next_change_at |
string | may be null; Earliest time of the next announce or withdraw; null when possible now or not yours to switch |
Pool¶
| Field | Type | Notes |
|---|---|---|
scope |
string | Pool id, used in paths and operations |
name |
string | Display name |
ipv4_range |
string | IPv4 range of the pool |
ipv6_range |
string | IPv6 range of the pool |
total |
integer | IPv4 addresses in the range, first and last included |
assigned |
integer | Addresses on your vServices |
blocked |
integer | Addresses on your block list |
reserved |
integer | Addresses withheld by Zero Services |
free |
integer | Addresses neither assigned, reserved nor blocked |
customer_announce |
boolean | You switch the announcement |
announced |
boolean | The edges announce the prefixes now |
announce_changed_at |
string | may be null; When you last switched the announcement; null if never |
next_change_at |
string | may be null; Earliest time of the next announce or withdraw; null when possible now or not yours to switch |
addresses |
list of object | Every IPv4 address of the pool |
VipOperations¶
| Field | Type | Notes |
|---|---|---|
operations (required) |
list of Operation |
at most 500; Up to 500 operations, applied in order; all succeed or none is applied |
idempotency_key |
string | up to 128 characters; Same as the Idempotency-Key header; when both are sent they must be equal |
Operation¶
| Field | Type | Notes |
|---|---|---|
op |
Fields |
|---|---|
assign |
service, and address or pool with count: the address unused the longest |
release |
address |
move |
address, to: another of your vServices |
swap |
address; optional new_address or pool, default the next address of the same pool; block_old puts the old address on the block list |
rotate_prefix |
from_pool, to_pool; keep_host keeps .17 as .17 where it is free |
block, unblock |
addresses: never hand them out again, or again |
announce, withdraw |
pool: only standby pools you switch yourself |
VipOperationRecord¶
| Field | Type | Notes |
|---|---|---|
id |
string | Operation id for GET /vip-operations/{op_id} |
idempotency_key |
string | may be null; The Idempotency-Key sent; null without one |
at |
string | When the operations were applied |
by |
string | Who ran them; key: |
services |
list of string | vServices the operations changed |
pools |
list of string | Pools whose announcement changed |
results |
list of object | One result per operation, in order: the op and what it did |
commit |
string | may be null; Configuration revision holding the change |
replayed |
boolean | true: a retry with the same Idempotency-Key, answered with the first result |
VipOperationStatus¶
| Field | Type | Notes |
|---|---|---|
id |
string | Operation id for GET /vip-operations/{op_id} |
idempotency_key |
string | may be null; The Idempotency-Key sent; null without one |
at |
string | When the operations were applied |
by |
string | Who ran them; key: |
services |
list of string | vServices the operations changed |
pools |
list of string | Pools whose announcement changed |
results |
list of object | One result per operation, in order: the op and what it did |
commit |
string | may be null; Configuration revision holding the change |
replayed |
boolean | true: a retry with the same Idempotency-Key, answered with the first result |
state |
string | one of pending, live, unknown; live: every reporting edge runs the change. pending: written, not yet live everywhere. unknown: the revision was not found |
edges |
list of object | Every edge that serves the changed vServices or announces the changed pools |
Network¶
| Field | Type | Notes |
|---|---|---|
vips |
list of Vip |
at most 16; read-only; Change addresses with POST /vip-operations |
pool |
string | may be null; read-only; Change addresses with POST /vip-operations |
edge_hostname |
string | may be null; read-only; Change addresses with POST /vip-operations |
Vip¶
| Field | Type | Notes |
|---|---|---|
anycast_ipv4 |
string | may be null; read-only; Change addresses with POST /vip-operations |
anycast_ipv6 |
string | may be null; read-only; Change addresses with POST /vip-operations |
Example¶
Swap an address, then check whether the change is live:
curl -s -X POST -H "Authorization: Bearer $ZP_KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" $API/vip-operations \
-d '{"operations": [{"op": "swap", "address": "198.51.100.17"}]}'
curl -s -H "Authorization: Bearer $ZP_KEY" $API/vip-operations/<id> | jq -r .state
Move everything from standby pool A to a fresh pool B:
# 1. announce B and wait until the operation is live
curl -s -X POST -H "Authorization: Bearer $ZP_KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" $API/vip-operations \
-d '{"operations": [{"op": "announce", "pool": "B"}]}'
# 2. move every address of A to B and withdraw A, in one step
curl -s -X POST -H "Authorization: Bearer $ZP_KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" $API/vip-operations \
-d '{"operations": [{"op": "rotate_prefix", "from_pool": "A", "to_pool": "B"},
{"op": "withdraw", "pool": "A"}]}'
Traffic of a pool¶
GET /pools/{scope}/metrics sums the same metrics as GET /metrics over the vServices of one of your pools: those with every address in it.
curl -s -H "Authorization: Bearer $ZP_KEY" "$API/pools/A/metrics?metric=traffic&range=24h"
curl -s -H "Authorization: Bearer $ZP_KEY" "$API/pools/A/metrics?metric=traffic_by_service"
- Only pools of your own range; any other pool is
404. - A vService with addresses in more than one pool is not summed in any of them, its counters are per vService.
scope.partial_servicesnames it.
Behaviour¶
- All operations of one request are applied together or not at all.
- Send an
Idempotency-Keywith every request: a retry after a timeout returns the first result (200,replayed: true) instead of changing twice. stateisliveonce every edge that serves the changed vServices or pools runs it, usually within a minute.- Keys of a delegation cannot use IP pools.
- Standby pools are validated in advance (LOA, route object, ROA, upstream filters) and stay dark until you announce them:
customer_announcemeans you switch the pool,announcedthat the edges announce it now. - Addresses come only from an announced pool; a pool is withdrawn only when none of its addresses is assigned.
- The announcement of one pool changes at most once every 15 minutes;
next_change_atsays when it may change again. More often, upstream networks may suppress the prefix. livemeans our edges announce the prefix; how fast the rest of the internet follows depends on the upstream networks.
In the portal: ZERO-PROTECT → IP Pools (main account and members with access; not delegations), with the traffic of each pool on its page.