Saltar al contenido principal

API Reference

All /api/vpn/* endpoints share these conventions:

  • Auth: Bearer token (Authorization: Bearer zsm_...) or signed session cookie. Write endpoints require ownership of the machine they act on.
  • Rate limit: 60/min on handshake/terminate, 240/min on reads.
  • Time: Every response includes zeqond (integer) and phase (0..1, 4 decimals). Bit-exact against lib/zeq-kernel-constants.ts.
  • Errors: { error, code }. HTTP status reflects failure class (400 invalid, 401 unauthorized, 403 not-party, 404 not-found, 409 invalid-state, 402 insufficient-balance).

POST /api/vpn/tunnel/handshake-request

Open a pending tunnel row. Requires the requester to own requester_slug and to hold enough ZEQ to cover 100 Zeqonds at the chosen rate.

// Request
{
"requester_slug": "ZEQ07000000001",
"target_slug": "ZEQ07000000007",
"requested_lifespan_zeqonds": 1000, // [10, 86_400]
"cover_traffic": false,
"multi_hop": false,
"pricing_unit": "gb" // "gb" | "zeqond"
}

// Response — 200 OK
{
"ok": true,
"tunnel_id": "8f7e6d5c-…",
"state": "pending",
"key_purpose_id": "vpn.tunnel.8f7e6d5c-….key",
"rate_zeq_per_unit": 50,
"min_required_zeq": 5000,
"balance_zeq": 12345,
"subscribers_notified": 3,
"zeqond": 2293617891,
"phase": 0.4117,
"pulse_hz": 1.2870016419610952
}

Error codes: INVALID_TARGET, INVALID_PRICING_UNIT, INVALID_LIFESPAN, TARGET_NOT_FOUND, TARGET_NOT_RELAY, TARGET_FULL, INSUFFICIENT_BALANCE, UNAUTHORIZED, NOT_OWNER.

POST /api/vpn/tunnel/handshake-accept

// Request — auth must own `accepter_slug`
{ "tunnel_id": "8f7e…", "accepter_slug": "ZEQ07000000007" }

// Response — 200 OK
{
"ok": true,
"tunnel_id": "8f7e…",
"state": "active",
"key_purpose_id": "vpn.tunnel.8f7e….key",
"started_at_zeqond": 2293617900,
"encrypted_key_fetch_url": "/api/zsc/get/vpn.tunnel.8f7e%E2%80%A6.key"
}

The shared key is derived as HMAC_SHA256( ZEQ_MESH_SECRET, requesterMachineId | accepterMachineId | nonce | zeqond ) and parked in zsc_secrets under key_purpose_id. Both peers fetch it with their existing ZSC-permission gate (the requester's ZID is added to the secret's permissions array at write time).

POST /api/vpn/tunnel/:id/terminate

Either party may terminate. Settlement math:

per-GB: ceil(cumulative_bytes / 1e9) × price_per_gb_zeq
per-Zeqond: cumulative_zeqonds × price_per_zeqond_zeq
// Request — auth must own one of (requester_slug, accepter_slug)
{
"terminator_slug": "ZEQ07000000001",
"cumulative_bytes": 734003200,
"cumulative_zeqonds": 472
}

// Response
{
"ok": true,
"tunnel_id": "8f7e…",
"state": "terminated",
"cumulative_bytes": 734003200,
"cumulative_zeqonds": 472,
"total_zeq_charged": 50, // ceil(0.734 GB) × 50 ZEQ/GB
"ended_at_zeqond": 2293618372
}

GET /api/vpn/tunnels?slug=<X>

Owner's last 100 tunnels (active + recent), most recent first. Auth required; caller must own slug.

GET /api/vpn/marketplace

Anonymous-readable. Filterable by:

  • country (e.g. EU, US) — matches accept_country_classes or ANY.
  • max_zeq_per_gb — price ceiling.

Returns only peers with an active=true row in state_machine_relay_offers and whose underlying state machine is is_public=true + status=active. Private machines are invisible.

POST /api/vpn/marketplace/list

Owner opts a machine into the public marketplace. Idempotent (onConflictDoUpdate).

{
"slug": "ZEQ07000000007",
"price_per_gb_zeq": 50,
"price_per_zeqond_zeq": 1,
"max_concurrent_tunnels": 8,
"accept_country_classes": ["EU", "ANY"],
"active": true // false = unlist
}