REST API

IP Transit API v1.0.0

Programmatically manage your Shulker IP Transit services. List and inspect your IPs, monitor live tunnel traffic, pull billing history, rename services, and generate one-time install / remove commands — all through a single REST endpoint secured by your existing Shulker API keys.

Updated recently 22 min read Production Ready

Introduction

IP Inventory
List every IP on your account with lifecycle status, expiry countdown, and region — all scoped to your API key owner.
Live Monitoring
Real-time online/offline state plus RX/TX throughput and byte counters, straight from the tunnel backend.
One-Time Commands
Generate short-lived install and remove commands for attaching an IP to your own server. Single-use, expiring.

The IP Transit API shares the same API key infrastructure as the Shulker API v2. A key only gains access to this API if its permissions JSON contains an ip_transit array — configured from the API Keys dashboard. No separate credentials are required.

Base URL
https://shulker.in/api/ip_transit-v1.0/

📡 Single endpoint — every request goes to the same URL. The operation is selected with the action query parameter (defaults to info). Parameters may be sent as a query string, application/x-www-form-urlencoded, or a JSON body.

Authentication

All requests require an active API key. Three transports are accepted — the header methods are strongly recommended because query strings end up in proxy and server logs.

X-API-Key header (recommended)
X-API-Key: sk_1e14aecda1a06bc22062f8fcb8e90cbde309d29d1e7955378b1aeaf6c130cdc3
Authorization: Bearer
Authorization: Bearer sk_1e14aecda1a06bc22062f8fcb8e90cbde309d29d1e7955378b1aeaf6c130cdc3
Query parameter (works, but logged)
?api_key=sk_1e14aecda1a06bc22062f8fcb8e90cbde309d29d1e7955378b1aeaf6c130cdc3

Key validation rules

CheckBehaviour
is_activeKey must exist and be active. Otherwise 401 invalid_api_key.
expires_atIf the key has an expiry in the past it is auto-deactivated and 401 api_key_expired is returned.
ip_restrictionsIf the key has CIDR/range restrictions, the caller IP must match one of them or 403 ip_not_allowed.
permissions.ip_transitMust be an array containing the required permission or "all". Otherwise 403 insufficient_permissions.

🔐 Security Note — API keys grant control over live network attachments. Treat them like passwords. Never commit keys to version control. Use environment variables or a secret manager, restrict keys by IP where possible, and rotate them every 90 days.

Permissions System

IP Transit permissions live in their own namespace inside the key's permissions JSON, under the ip_transit key. Granting "all" enables every action. The dashboard writes this array for you.

permissions JSON on the API key
{
  "read": [],
  "write": [],
  "ip_transit": ["list", "view", "monitor", "history"]
}
PermissionGrants access to
listList all IPs on the account
viewView details of a single IP
monitorLive online/offline status and traffic metrics
aliasRename (set the alias of) an IP
install_commandGenerate one-time install / remove commands
historyView billing and renewal history
allAll IP Transit permissions (superset)

ℹ️ No permission required — info and permissions are always available to any key that can authenticate, so you can use them to discover what a key is allowed to do.

Rate Limits

Limits are stored per key and enforced across three sliding windows. Defaults are shown — a key may have tighter custom limits configured in the dashboard.

60 / minute
Default per key
1,000 / hour
Default per key
10,000 / day
Default per key

⚠️ Stricter limit on install_command — generating install/remove commands is capped at 6 requests per minute per key, independent of the general limits, because the commands are sensitive. Exceeding it returns 429 rate_limited with Retry-After: 60.

Response headers
X-RateLimit-Remaining-Minute: 47
X-Request-Id: req_9f2c1a7b3d4e5f60
Retry-After: 60            # only present on 429
_meta.rate_limit in the response body
"_meta": {
  "request_id": "req_9f2c1a7b3d4e5f60",
  "timestamp": "2026-01-07T16:28:03+00:00",
  "response_time": "0.0412s",
  "version": "1.0.0",
  "rate_limit": {
    "minute_remaining": 47,
    "hour_remaining": 982,
    "day_remaining": 9961
  }
}

Error Handling

Every response — success or failure — is JSON and carries a success boolean and a _meta object with a request ID. Failed requests additionally carry error, error_code, and code.

HTTP status codes

StatusMeaning
200 OKRequest successful
400 Bad RequestInvalid or missing parameters
401 UnauthorizedMissing, invalid, or expired API key
403 ForbiddenInsufficient permissions, IP restriction, suspended or expired IP
404 Not FoundUnknown action, or IP not found on this account
405 Method Not AllowedWrong HTTP verb for the action (see Allow header)
429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorUnexpected server error
502 Bad GatewayUpstream tunnel backend unavailable

Error codes

error_codeHTTPWhen it happens
missing_api_key401No key found in header, bearer token, or query string
invalid_api_key401Key does not exist or is inactive
api_key_expired401Key expiry has passed (key is auto-deactivated)
ip_not_allowed403Caller IP does not match the key's IP restrictions
insufficient_permissions403Key lacks the required ip_transit permission
ip_suspended403Install command requested for a suspended IP
ip_expired403Install command requested for an expired IP
validation_error400Bad id, ip, status, or alias
not_found404IP does not exist on this account
unknown_action404Action not in the route table (response lists valid actions)
method_not_allowed405Correct action but wrong HTTP method
rate_limited429Any rate limit exceeded
upstream_unavailable502Tunnel backend could not be reached
internal_error500Unhandled server exception
Error response (permission denied)
{
  "success": false,
  "error": "This API key does not have permission for this action.",
  "error_code": "insufficient_permissions",
  "code": 403,
  "required_permission": "monitor",
  "granted_permissions": ["list", "view"],
  "_meta": {
    "request_id": "req_9f2c1a7b3d4e5f60",
    "timestamp": "2026-01-07T16:28:03+00:00",
    "response_time": "0.0021s",
    "version": "1.0.0",
    "rate_limit": {
      "minute_remaining": 59,
      "hour_remaining": 999,
      "day_remaining": 9999
    }
  }
}

🆔 Debugging — always log _meta.request_id together with the status code. Support can trace the exact request, key, and response using that ID. When contacting support, include the request ID and the timestamp.

IP Status & Lifecycle

Every IP returned by the API has a computed status. The rules match the dashboard exactly, so both always agree.

StatusCondition
activeNot suspended, not expired, and more than 7 days of runtime remaining
expiringNot suspended, not expired, and 7 days or fewer remaining
expiredexpires_at is in the past. A 3-day grace period is kept for the owner; grace_ends_at is populated.
suspendedSuspended either in the database or upstream on the tunnel backend. suspension_reason may be present.

🕒 Grace period — an expired IP is kept on the account for 3 days (grace_ends_at). After that it is released. You cannot generate install commands for expired IPs — renew first.

📡 Live data cache — the upstream tunnel list is cached server-side for 4 seconds to protect the upstream provider's 120 req/min budget. Rapid polling within that window returns the same figures — it does not violate your own rate limits.

API Info & Permissions

Discover the API version, the capabilities of the calling key, its rate limits, and the full list of actions. No permission required — any authenticated key may call these.

GET ?action=info
none
Request
curl "https://shulker.in/api/ip_transit-v1.0/?action=info" \
  -H "X-API-Key: sk_..."
Response 200
{
  "success": true,
  "api": "Shulker IP Transit API",
  "version": "1.0.0",
  "documentation": "/docs/ip-transit/",
  "key": {
    "name": "Monitoring Bot",
    "expires_at": "2026-04-01T00:00:00Z",
    "rate_limits": { "per_minute": 30, "per_hour": 500, "per_day": 5000 }
  },
  "granted_permissions": ["list", "view", "monitor"],
  "all_access": false,
  "available_permissions": {
    "list": "List all IPs on the account",
    "view": "View details of a single IP",
    "monitor": "Live online/offline status and traffic",
    "alias": "Rename (set alias of) IPs",
    "install_command": "Generate one-time install / remove commands",
    "history": "View billing and renewal history",
    "all": "All IP Transit permissions"
  },
  "actions": {
    "info":            { "permission": null, "methods": ["GET"] },
    "list":            { "permission": "list", "methods": ["GET"] },
    "get":             { "permission": "view", "methods": ["GET"] },
    "monitor":         { "permission": "monitor", "methods": ["GET"] },
    "history":         { "permission": "history", "methods": ["GET"] },
    "set_alias":       { "permission": "alias", "methods": ["POST", "PUT", "PATCH"] },
    "install_command": { "permission": "install_command", "methods": ["POST"] }
  },
  "_meta": { "request_id": "req_...", "version": "1.0.0" }
}

💡 Tip — ?action=permissions is an alias of info and returns the exact same payload. Use it if you prefer a self-describing URL.

List IPs

Returns every active IP on the account with a status summary. The live block is included only if the key has the monitor permission; otherwise it is omitted.

GET ?action=list
list
ParameterTypeDescription
actionstringlist
statusstringOptional. Filter by active, expiring, expired, or suspended.
Request
curl "https://shulker.in/api/ip_transit-v1.0/?action=list" \
  -H "X-API-Key: sk_..."

# with a filter
curl "https://shulker.in/api/ip_transit-v1.0/?action=list&status=expiring" \
  -H "X-API-Key: sk_..."
Response 200
{
  "success": true,
  "count": 2,
  "summary": {
    "total": 2,
    "active": 1,
    "expiring": 1,
    "expired": 0,
    "suspended": 0
  },
  "ips": [
    {
      "id": 42,
      "ip": "203.0.113.10",
      "alias": "Edge Frankfurt",
      "region": "de0",
      "status": "active",
      "suspended": false,
      "suspension_reason": null,
      "created_at": "2025-11-02T09:14:00Z",
      "expires_at": "2026-02-02T09:14:00Z",
      "last_billed_at": "2026-01-02T09:14:00Z",
      "days_left": 26,
      "grace_ends_at": null,
      "live": { "online": true, "rx_mbps": 12.482, "tx_mbps": 3.117 }
    },
    {
      "id": 41,
      "ip": "198.51.100.7",
      "alias": null,
      "region": "us1",
      "status": "expiring",
      "suspended": false,
      "suspension_reason": null,
      "created_at": "2025-12-08T18:02:11Z",
      "expires_at": "2026-01-13T18:02:11Z",
      "last_billed_at": "2026-01-06T18:02:11Z",
      "days_left": 6,
      "grace_ends_at": null,
      "live": { "online": false, "rx_mbps": 0, "tx_mbps": 0 }
    }
  ],
  "_meta": { "request_id": "req_...", "version": "1.0.0" }
}

ℹ️ Note — summary always reflects all IPs regardless of the status filter. Use it to build dashboards without a second request.

Get Single IP

Fetch one IP by id or by its public ip address. Always scoped to the key's owner — IPs belonging to other accounts return 404.

GET ?action=get
view
ParameterTypeDescription
idintegerEither this or ip. The service ID.
ipstringEither this or id. The public IPv4 address.
Request
curl "https://shulker.in/api/ip_transit-v1.0/?action=get&id=42" \
  -H "X-API-Key: sk_..."

# or by IP
curl "https://shulker.in/api/ip_transit-v1.0/?action=get&ip=203.0.113.10" \
  -H "X-API-Key: sk_..."
Response 200
{
  "success": true,
  "ip": {
    "id": 42,
    "ip": "203.0.113.10",
    "alias": "Edge Frankfurt",
    "region": "de0",
    "status": "active",
    "suspended": false,
    "suspension_reason": null,
    "created_at": "2025-11-02T09:14:00Z",
    "expires_at": "2026-02-02T09:14:00Z",
    "last_billed_at": "2026-01-02T09:14:00Z",
    "days_left": 26,
    "grace_ends_at": null,
    "live": { "online": true, "rx_mbps": 12.482, "tx_mbps": 3.117 }
  },
  "_meta": { "request_id": "req_...", "version": "1.0.0" }
}

⚠️ Validation — id must be numeric and ip must be a valid IP address, otherwise 400 validation_error. Passing both is allowed; id takes precedence.

Live Monitoring

Returns live tunnel state and traffic. Call it with id/ip for a single IP (full detail including subnets and byte counters), or without for a compact snapshot of every IP on the account.

GET ?action=monitor
monitor
ParameterTypeDescription
idintegerOptional. Single-IP mode.
ipstringOptional. Single-IP mode by address.
Request — single IP
curl "https://shulker.in/api/ip_transit-v1.0/?action=monitor&id=42" \
  -H "X-API-Key: sk_..."
Response — single IP
{
  "success": true,
  "ip": { "id": 42, "ip": "203.0.113.10", "alias": "Edge Frankfurt", "status": "active" },
  "live": {
    "online": true,
    "protocol": "wireguard",
    "tunnel_ip": "10.64.0.2",
    "rx_mbps": 12.482,
    "tx_mbps": 3.117,
    "rx_bytes": 1849203817,
    "tx_bytes": 482019337,
    "subnets": [
      { "kind": "ipv4", "cidr": "203.0.113.0/28" }
    ]
  },
  "_meta": { "request_id": "req_...", "version": "1.0.0" }
}
Request — all IPs
curl "https://shulker.in/api/ip_transit-v1.0/?action=monitor" \
  -H "X-API-Key: sk_..."
Response — all IPs
{
  "success": true,
  "count": 2,
  "ips": [
    {
      "id": 42,
      "ip": "203.0.113.10",
      "alias": "Edge Frankfurt",
      "status": "active",
      "online": true,
      "rx_mbps": 12.482,
      "tx_mbps": 3.117
    },
    {
      "id": 41,
      "ip": "198.51.100.7",
      "alias": null,
      "status": "expiring",
      "online": false,
      "rx_mbps": 0,
      "tx_mbps": 0
    }
  ],
  "_meta": { "request_id": "req_...", "version": "1.0.0" }
}

📉 Upstream failures — if the tunnel backend cannot be reached, the API returns 502 upstream_unavailable rather than stale or partial data. Retry with a short backoff.

Billing History

Returns billing and renewal history for the account, newest first. Optionally scoped to a single IP.

GET ?action=history
history
ParameterTypeDescription
id / ipint / stringOptional. Scope history to a single IP.
limitintegerOptional. Number of records, default 50, max 200.
Request
curl "https://shulker.in/api/ip_transit-v1.0/?action=history&id=42&limit=20" \
  -H "X-API-Key: sk_..."
Response 200
{
  "success": true,
  "count": 1,
  "history": [
    {
      "ip_id": 42,
      "ip": "203.0.113.10",
      "type": "renew",
      "billing_period": "1_month",
      "months": 1,
      "amount_usd": 4.5,
      "period_start": "2026-01-02T09:14:00Z",
      "period_end": "2026-02-02T09:14:00Z",
      "created_at": "2026-01-02T09:14:22Z"
    }
  ],
  "_meta": { "request_id": "req_...", "version": "1.0.0" }
}

Set Alias

Rename an IP with a human-friendly alias. Send an empty string to clear an existing alias. Accepts POST, PUT, or PATCH.

POST PUT PATCH ?action=set_alias
alias
ParameterTypeDescription
id / ipint / stringRequired. Which IP to rename.
aliasstringRequired. Up to 40 chars: letters, numbers, spaces, -, _, .. Empty string clears it.
Request
curl -X POST https://shulker.in/api/ip_transit-v1.0/ \
  -H "X-API-Key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "set_alias",
    "id": 42,
    "alias": "Edge Frankfurt"
  }'
Response 200
{
  "success": true,
  "id": 42,
  "ip": "203.0.113.10",
  "alias": "Edge Frankfurt",
  "_meta": { "request_id": "req_...", "version": "1.0.0" }
}

📝 Audit trail — alias changes are recorded in the IP Transit event log with the key ID that made the change, so you can always trace who renamed what.

Install / Remove Command

Generates a one-time install command and a matching remove command for attaching the IP to your own server. The command is single-use and expires — treat it like a password.

POST ?action=install_command
install_command
ParameterTypeDescription
id / ipint / stringRequired. Target IP.
ttl_hoursintegerOptional. Command lifetime, default 24, range 1–168. Alias: ttl.
Request
curl -X POST https://shulker.in/api/ip_transit-v1.0/ \
  -H "X-API-Key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "install_command",
    "id": 42,
    "ttl_hours": 24
  }'
Response 200
{
  "success": true,
  "ip": { "id": 42, "ip": "203.0.113.10" },
  "install_command": "curl -fsSL https://shulker.in/cdn/ip-transit/install.sh | sudo bash -s -- --token ipt_7f3a...",
  "remove_command": "curl -fsSL https://shulker.in/cdn/ip-transit/install.sh | sudo bash -s -- --remove --token ipt_7f3a...",
  "expires_at": "2026-01-08T16:28:03Z",
  "ttl_hours": 24,
  "warning": "This command works once and anyone who has it can attach this IP to their server. Treat it like a password.",
  "_meta": { "request_id": "req_...", "version": "1.0.0" }
}

⚠️ Sensitive — install commands are rate-limited to 6 per minute per key. They cannot be generated for suspended or expired IPs (403 ip_suspended / 403 ip_expired). The response is never stored in the API usage log — it is redacted as [redacted: contains a one-time install command].

🔐 Treat as a secret — anyone holding the install command can attach the IP to their own server. Deliver it over a secure channel, use it immediately, and never paste it into public channels or logs.

Response Objects

IP object

FieldTypeDescription
idintegerService ID — use it as id= in subsequent calls
ipstringPublic IPv4 address
aliasstring|nullHuman-friendly name, or null when unset
regionstringNode/region code (e.g. de0, us1)
statusstringactive, expiring, expired, or suspended
suspendedbooleanTrue if suspended in DB or upstream
suspension_reasonstring|nullReason, when present
created_atstringISO 8601 creation time
expires_atstring|nullISO 8601 expiry time
last_billed_atstring|nullISO 8601 last billing time
days_leftinteger|nullDays until expiry (negative when past)
grace_ends_atstring|nullEnd of the 3-day grace window, only when expired
liveobject{ online, rx_mbps, tx_mbps } — omitted without monitor permission

Live tunnel object

FieldTypeDescription
onlinebooleanTunnel is currently up
protocolstringTunnel protocol (e.g. wireguard)
tunnel_ipstringPeer IP of the tunnel
rx_mbpsnumberCurrent receive throughput, Mbps
tx_mbpsnumberCurrent transmit throughput, Mbps
rx_bytesintegerBytes received this period
tx_bytesintegerBytes transmitted this period
subnetsarray[{ kind, cidr }] routed subnets

History object

FieldTypeDescription
ip_idintegerService ID the record belongs to
ipstringPublic IP address
typestringBilling action (e.g. renew)
billing_periodstringTenure label (e.g. 1_month)
monthsintegerNumber of months billed
amount_usdnumberAmount charged, USD
period_startstring|nullISO 8601 period start
period_endstring|nullISO 8601 period end
created_atstringISO 8601 record creation

_meta object

FieldTypeDescription
request_idstringUnique request identifier — quote it to support
timestampstringISO 8601 response time (UTC)
response_timestringServer processing time, e.g. 0.0412s
versionstringAPI version (1.0.0)
rate_limitobject{ minute_remaining, hour_remaining, day_remaining }

Code Examples

Python Client
import os
import requests
from typing import Optional, Dict, Any


class IPTransitAPI:
    def __init__(self, api_key: str,
                 base_url: str = "https://shulker.in/api/ip_transit-v1.0/"):
        self.api_key = api_key
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            "X-API-Key": api_key,
            "Accept": "application/json",
        })

    def _request(self, params: Dict[str, Any], method: str = "GET") -> Dict[str, Any]:
        if method.upper() == "GET":
            response = self.session.get(self.base_url, params=params)
        else:
            response = self.session.request(method, self.base_url, json=params)
        return response.json()

    def info(self) -> Dict[str, Any]:
        return self._request({"action": "info"})

    def list_ips(self, status: Optional[str] = None) -> Dict[str, Any]:
        params = {"action": "list"}
        if status:
            params["status"] = status
        return self._request(params)

    def get_ip(self, ip_id: Optional[int] = None,
               ip: Optional[str] = None) -> Dict[str, Any]:
        params = {"action": "get"}
        if ip_id is not None:
            params["id"] = ip_id
        elif ip:
            params["ip"] = ip
        return self._request(params)

    def monitor(self, ip_id: Optional[int] = None,
                ip: Optional[str] = None) -> Dict[str, Any]:
        params = {"action": "monitor"}
        if ip_id is not None:
            params["id"] = ip_id
        elif ip:
            params["ip"] = ip
        return self._request(params)

    def history(self, ip_id: Optional[int] = None,
                limit: int = 50) -> Dict[str, Any]:
        params = {"action": "history", "limit": limit}
        if ip_id is not None:
            params["id"] = ip_id
        return self._request(params)

    def set_alias(self, ip_id: int, alias: str) -> Dict[str, Any]:
        return self._request(
            {"action": "set_alias", "id": ip_id, "alias": alias},
            method="POST",
        )

    def install_command(self, ip_id: int, ttl_hours: int = 24) -> Dict[str, Any]:
        return self._request(
            {"action": "install_command", "id": ip_id, "ttl_hours": ttl_hours},
            method="POST",
        )


# Usage
if __name__ == "__main__":
    client = IPTransitAPI(os.environ["SHULKER_API_KEY"])

    # Discover what this key can do
    info = client.info()
    if not info["success"]:
        raise SystemExit(info["error"])

    print(f"Key: {info['key']['name']} — perms: {info['granted_permissions']}")

    # List every IP, summarised
    listing = client.list_ips()
    for entry in listing["ips"]:
        print(f"{entry['ip']:16} {entry['status']:10} {entry['days_left']}d left")

    # Alert on anything not healthy
    for entry in listing["ips"]:
        if entry["status"] in ("expiring", "expired", "suspended"):
            print(f"WARN: {entry['ip']} is {entry['status']}")

    # Live traffic snapshot
    live = client.monitor()
    for entry in live.get("ips", []):
        print(f"{entry['ip']} online={entry['online']} rx={entry['rx_mbps']}Mbps tx={entry['tx_mbps']}Mbps")
JavaScript / Node.js Client
class IPTransitAPI {
  constructor(apiKey, baseUrl = 'https://shulker.in/api/ip_transit-v1.0/') {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
  }

  async request(params, method = 'GET') {
    const url = new URL(this.baseUrl);

    if (method === 'GET') {
      url.search = new URLSearchParams(params).toString();
      const res = await fetch(url, { headers: { 'X-API-Key': this.apiKey } });
      return res.json();
    }

    const res = await fetch(url, {
      method,
      headers: {
        'X-API-Key': this.apiKey,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(params),
    });
    return res.json();
  }

  info()                { return this.request({ action: 'info' }); }
  listIPs(status)       { return this.request({ action: 'list', ...(status ? { status } : {}) }); }
  getIP(id)             { return this.request({ action: 'get', id }); }
  monitor(id)           { return this.request({ action: 'monitor', ...(id ? { id } : {}) }); }
  history(id, limit=50) { return this.request({ action: 'history', id, limit }); }
  setAlias(id, alias)   { return this.request({ action: 'set_alias', id, alias }, 'POST'); }
  installCommand(id, ttlHours = 24) {
    return this.request({ action: 'install_command', id, ttl_hours: ttlHours }, 'POST');
  }
}

// Usage
async function main() {
  const client = new IPTransitAPI(process.env.SHULKER_API_KEY);

  const info = await client.info();
  if (!info.success) throw new Error(info.error);

  console.log(`Key ${info.key.name} → ${info.granted_permissions.join(', ')}`);

  const listing = await client.listIPs();
  listing.ips.forEach(ip => {
    console.log(`${ip.ip}  ${ip.status}  ${ip.days_left}d left`);
  });

  // Rename an IP
  const renamed = await client.setAlias(42, 'Edge Frankfurt');
  console.log(`Alias set to: ${renamed.alias}`);

  // Live snapshot
  const live = await client.monitor();
  live.ips.forEach(ip => {
    console.log(`${ip.ip} online=${ip.online} rx=${ip.rx_mbps} tx=${ip.tx_mbps}`);
  });
}

main().catch(err => { console.error(err); process.exit(1); });
Bash — health check + reinstall flow
#!/usr/bin/env bash
set -euo pipefail

API_KEY="${SHULKER_API_KEY:?set SHULKER_API_KEY first}"
BASE="https://shulker.in/api/ip_transit-v1.0/"
IP_ID="42"

api() {
  # $1 = query string (no leading ?)
  curl -fsS "${BASE}?$1" -H "X-API-Key: ${API_KEY}"
}

api_post() {
  # $1 = JSON body
  curl -fsS -X POST "${BASE}" \
    -H "X-API-Key: ${API_KEY}" \
    -H "Content-Type: application/json" \
    -d "$1"
}

echo "→ Listing IPs"
api "action=list" | jq -r '
  "Total: \(.summary.total)  active: \(.summary.active)  expiring: \(.summary.expiring)",
  (.ips[] | "  \(.ip)\t\(.status)\t\(.days_left)d left")'

echo "→ Live status for IP #${IP_ID}"
api "action=monitor&id=${IP_ID}" | jq -r '
  "online=\(.live.online) rx=\(.live.rx_mbps)Mbps tx=\(.live.tx_mbps)Mbps"'

echo "→ Generating a fresh install command (TTL 1h)"
INSTALL=$(api_post "{
  \"action\": \"install_command\",
  \"id\": ${IP_ID},
  \"ttl_hours\": 1
}" | jq -r '.install_command')

# Run it on the server that should announce the IP.
echo "Run on your server:"
echo "  ${INSTALL}"

Best Practices

🔐 Security — keep API keys in environment variables or a secret manager, restrict keys with IP allow-lists, grant only the ip_transit permissions you need (e.g. monitoring bots rarely need install_command), and rotate keys every 90 days.

⚡ Polling — the tunnel list is cached server-side for 4 seconds, so polling monitor more frequently than once every ~5 seconds adds no information. For dashboards, poll once per 10–15 s and reuse the response; for alerting, prefer checking only status=expiring and status=suspended.

🔄 Retries — retry 429 and 502 with exponential backoff plus jitter, honouring the Retry-After header on 429. Do not retry 400, 401, 403, or 404 — those are deterministic and will keep failing.

🧾 Idempotency — install_command and set_alias accept POST, PUT, and PATCH where applicable. Because install commands are single-use and rate-limited to 6/min, never auto-retry them blindly — generate a new one instead.

🆔 Observability — log _meta.request_id alongside the status code for every call. It is the fastest way for support to trace a specific request on our side.

Changelog

v1.0.0 — Initial release — Public launch of the Shulker IP Transit API. Reuses existing Shulker API keys with a dedicated ip_transit permission namespace. Added info, list, get, monitor, history, set_alias, and install_command actions. Live tunnel data sourced from the upstream backend with a 4-second server-side cache. Per-key sliding-window rate limits (minute / hour / day) plus a stricter 6/min cap on install commands. Install command responses are redacted from the API usage log. IP filtering by status with an always-complete summary block.

ℹ️ Compatibility — the IP Transit API shares authentication with Shulker API v2. Keys without an ip_transit permission will authenticate successfully but receive 403 insufficient_permissions on every action except info / permissions. Enable the permission from the API Keys dashboard.