---
title: "Self-enroll a node using a provisioning token (no API key)"
method: POST
path: "/v1/nodes/enroll"
tags: ["Internal"]
---

# Self-enroll a node using a provisioning token (no API key)

`POST /v1/nodes/enroll`

## Request body

- EnrollNodeRequest
  - `cpuCores` integer
  - `cpuModel` string
  - `gpuInventory` GpuInventory — Versioned, driverless GPU detection (lspci) collected by enroll.sh. Works on VFIO hosts with no NVIDIA driver and on non-NVIDIA accelerators. source=none means the node could not detect (no lspci): the control plane must NOT treat that as "no GPUs".
    - `devices` object[], required
      - `bdfs` string[] — full BDFs of the group's devices
      - `count` integer, required
      - `pciClass` string — PCI class code, e.g. 0300/0302/1200
      - `pciDeviceId` string, required — vendor:device, e.g. 10de:2204
      - `pciName` string — lspci device name string
    - `driverVersion` string
    - `iommuGroups` object[] — Per-IOMMU-group VFIO units: each entry is one IOMMU group containing an NVIDIA/AMD GPU. A group's GPUs are indivisible (must be passed together). Empty when IOMMU is off (gVisor/CPU nodes): those GPUs are fed via the Nomad device fingerprint instead. Kept out of the hardware fingerprint (BDFs renumber across reboots).
      - `gpus` object[], required — GPU-class (0300/0302) NVIDIA/AMD devices in this group, the sellable + passthrough set
        - `bdf` string, required — full BDF, e.g. 0000:04:00.0
        - `deviceId` string, required — vendor:device, e.g. 10de:2204
      - `group` integer, required — IOMMU group number (provenance)
    - `nvlinkActive` boolean — The DRIVER reports live NVLink between GPUs (NV# links in nvidia-smi topo -m). True on a virtualised SXM guest whose nvswitchCount is 0. Absent on older node scripts.
    - `nvswitchBdfs` string[] — Full BDFs of the node's NVSwitch functions (PCI class 0680, vendor 10de), e.g. "0000:83:00.0". Full-node (all-GPU) VM placement appends these to the passthrough list so the guest gets the NVLink fabric; partial allocations never receive them. Absent/empty on non-SXM boxes and old enroll scripts.
    - `nvswitchCount` integer, required — NVSwitch bridges (PCI class 0680, vendor 10de) VISIBLE ON THIS MACHINE'S PCI BUS. This answers "can we pass the switches through to a VM?", not "does NVLink work?": on a virtualised SXM platform the host keeps the switch functions and gives the guest an NVLink partition, so this is 0 while NVLink is fully live. Read nvlinkActive for the latter.
    - `source` string, required — none | lspci | lspci+nvidia-smi
    - `version` integer, required
  - `gpus` object[] — LEGACY nvidia-smi detection (name + pci.bus_id BDF, NOT a vendor:device id despite the example). Kept for old scripts; gpuInventory supersedes it.
    - `name` string
    - `pciId` string, required — nvidia-smi pci.bus_id (a BDF like 00000000:04:00.0)
  - `hardwareFingerprint` string, required
  - `hostShape` HostShape — What the machine ACTUALLY is, detected by enroll.sh. The platform supports two shapes and each has exactly one correct runtime: baremetal => qemu (we convert the GPUs to vfio-pci and rent VMs), guest => runc (we keep the native NVIDIA driver and run pods). The runtime follows the shape on the box, and the control plane refuses any enroll whose runtime contradicts it instead of recording a node that cannot serve its own product. Absent on older node scripts, which the control plane refuses (re-run the bootstrap).
    - `hasDevKvm` boolean — /dev/kvm exists. A node without it cannot host VMs no matter what its runtime says.
    - `hypervisor` string — Raw systemd-detect-virt output (kvm, vmware, none, ...); provenance for the shape.
    - `iommuGroupCount` integer — Entries in /sys/kernel/iommu_groups. 0 on a typical cloud guest, which is why pod nodes mint one unit per GPU instead of per IOMMU group.
    - `shape` string — baremetal | guest | unknown. unknown means systemd-detect-virt was unavailable; it is NOT treated as baremetal, because that is the direction that tears down a working driver.
  - `hostname` string
  - `identityPubkey` string — Node ed25519 identity public key (base64 raw, 32 bytes). The collision-safe node anchor: the box generates this once, keeps the private key at /etc/vectorlay/identity.key, and signs every enroll with it. Required by the server: an enroll without it is refused (401 BAD_SIGNATURE).
  - `machineId` string — Stable per-box identity (SMBIOS product_uuid / serial / cloud instance-id). Primary global node-identity key; fingerprint is the fallback when empty.
  - `nebulaPubkey` string, required — Node Nebula public key PEM (from nebula-cert keygen)
  - `ramGb` integer
  - `runtime` string — Node runtime, detected on the box by enroll.sh from the host shape: qemu (VFIO/KVM passthrough VM host) on baremetal, runc (GPU-container Pod node) on a cloud guest. The control plane refuses an enroll whose runtime is absent or contradicts hostShape (matched case-insensitively).
  - `signature` string — base64 ed25519 signature proving possession of identityPubkey over the identity-critical enroll fields (token hash, identity pubkey, nebula pubkey hash, fingerprint, machineId, hostname, signedAt). Required by the server: an enroll without it is refused (401 BAD_SIGNATURE).
  - `signedAt` integer — Unix seconds when the enroll body was signed. Required by the server: an unsigned enroll is refused (401 BAD_SIGNATURE); rejected if more than 300s from server time.
  - `token` string, required — Provisioning token (vtk_...)

## Response `200`

Enrolled

- EnrollNodeBundle
  - `dataPlaneCa` string, required — CertIssuer CA the gateway pins; optional
  - `dataPlaneCert` string, required — node data-plane mTLS cert (gateway tunnel); optional
  - `dataPlaneKey` string, required — node data-plane mTLS key; optional
  - `datacenter` string, required
  - `gatewayAddr` string, required — host:port the node-agent dials (QUIC/UDP); optional
  - `gatewayServerName` string, required — expected CN on the gateway server cert; optional
  - `nebulaCa` string, required
  - `nebulaCert` string, required
  - `nebulaLighthouses` object[], required
    - `endpoint` string, required — publicIp:4242
    - `nebulaIp` string, required
  - `nebulaOverlayIp` string, required
  - `nodeAgentToken` string — durable node token for the node-agent self-update fetch (GET /v1/nodes/agent); optional
  - `nodeId` string, required
  - `nomadCa` string, required
  - `nomadClientCert` string, required
  - `nomadClientKey` string, required
  - `nomadClientReset` boolean, required — The control plane has determined the node's Nomad record is gone (record GC'd past node_gc_threshold, or purged) and no live workload is pinned here, so enroll.sh must reset the local Nomad client (stop nomad, kill orphaned VM processes, delete the client state DB) before the fresh introduction token can register a new record. Never set while any workload is placed, so a running VM's processes are never stranded.
  - `nomadIntroToken` string, required
  - `nomadPool` string, required
  - `nomadServers` string[], required — server Nebula RPC addrs (ip:4647)

## Other responses

- `401` — Invalid/expired token
- `403` — Revoked
- `409` — Node busy (reassign blocked), hostname conflict, or a concurrent enroll to retry

---

[API](https://skmtc.net/openrelay/apis/openrelay-api.md) · [All operations](https://skmtc.net/openrelay/apis/openrelay-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/openrelay/openrelay-api/revisions/c8ee30a7762d/schema)
