---
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)
    - `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) on the node
    - `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
  - `hostname` string
  - `identityPubkey` string — Node ed25519 identity public key (base64 raw, 32 bytes). Identity v2 collision-safe anchor: the box generates this once, keeps the private key at /etc/vectorlay/identity.key, and signs every enroll with it. Optional — absent = legacy (v1) enroll.
  - `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 by enroll.sh: qemu (VFIO/KVM passthrough) or gvisor (container GPUs). Default gvisor.
  - `signature` string — base64 ed25519 signature (v2) proving possession of identityPubkey over the identity-critical enroll fields (token hash, identity pubkey, nebula pubkey hash, fingerprint, machineId, hostname, signedAt). Required iff identityPubkey is set.
  - `signedAt` integer — Unix seconds when the enroll body was signed (v2). 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
  - `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) or hostname conflict
- `429` — Quota exhausted

---

[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/versions/32905b8f44f8/schema)
