Updated May 8, 2026
All API endpoints (except /health and /ready) require authentication via API key.
API Key Authentication
Pass your API key in the X-API-Key header. Each key is bound to a specific account.
curl -H "X-API-Key: YOUR_API_KEY" \
https://api.rafftechnologies.com/api/v1/vms
| Header | Required | Description |
|---|
X-API-Key | Always | Your API key for authentication |
X-Project-ID | Mutating endpoints | Project ID for create, delete, power actions, resize, and other write operations |
Mutating operations (POST, DELETE, PATCH) return 400 Bad Request with "X-Project-ID required" if this header is missing.
curl -X POST https://api.rafftechnologies.com/api/v1/vms \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Project-ID: YOUR_PROJECT_ID" \
-H "Content-Type: application/json" \
-d '{"name": "my-vm", "template_id": "<template-uuid>", "pricing_id": 1, "region": "us-east"}'
API responses return the same standard fields:
{
"id": "69590a15-...",
"name": "ubuntu-2cpu-4gb-01",
"status": "active",
"cpu": 2,
"ram": 4,
"storage": 80,
"public_ipv4_address": "15.204.178.3",
"price_per_hour": "0.027764",
"pricing_id": 3,
"billing_type": "payg",
"region": "us-east",
"created_at": "2025-07-12T02:36:56Z"
}
Managing API Keys
API keys are managed from the Raff Dashboard:
- Click API Keys in the sidebar (it’s a top-level menu item — not under Settings)
- Click Create API Key
- Name the key, pick its Account Role (and Project Role if not Owner), set an expiration, and click Create Key
- Copy the key immediately — it’s only shown once
For the full create flow with roles and permissions preview, see Generate an API key.
Store your API keys securely. Never commit them to version control or expose them in client-side code.
Rate Limiting
Requests are rate-limited per API key. The tier is set on the key when it’s created (or by Raff support, on request).
| Tier | Requests/sec | Burst | Notes |
|---|
| Standard (default for all new keys) | 30 / sec | 60 | The right tier for typical dashboards, scripts, and automation — well above what most workloads sustain |
| High | 100 / sec | 200 | For tools that legitimately drive sustained throughput — bulk migrations, monitoring exporters scraping every endpoint, large fleet reconciliation loops |
How it actually works. The limiter is a sliding 1-second window plus a separate burst budget. As long as you stay within the per-second number, you’re fine. If you exceed it briefly, the burst budget covers you for short spikes — once that’s consumed, you start getting 429s until both windows recover. Burst budget refreshes on a 60-second window.
When rate-limited, the API returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
{
"error": "Rate limit exceeded",
"message": "Maximum 30 requests per second allowed for your tier"
}
Recommended client behavior:
- Honor the
Retry-After header. It tells you exactly when to retry — don’t guess
- Implement exponential backoff for
429 and 5xx: 1s → 2s → 4s → 8s → 16s capped at ~60s
- Coalesce reads — if you’re polling list endpoints every 100 ms in a loop, slow down. Most resources change on second-or-slower timescales
- Cache catalog data —
/api/v1/public/templates, /api/v1/public/pricing/vm, region lists, etc. don’t change minute-to-minute. Cache them locally for the lifetime of your script
Upgrading tier
Need more than Standard sustains? Two paths:
| Path | When |
|---|
| Email support@rafftechnologies.com | Production migration, large fleet bulk-create, tooling that legitimately needs sustained throughput. Tell us the workload and the target peak RPS — we’ll move the key to High if it’s reasonable |
| Stay on Standard | Anything human-driven (dashboards, ad-hoc scripts, occasional automation). Standard’s 30/sec + burst-of-60 covers it with room to spare |
There’s no self-service upgrade today — it’s a support request because we want to understand the use case (and stop accidental loops from flooding the platform).
Calling the API from HTTP clients
Raff doesn’t ship a first-party SDK today. The API is plain REST + JSON, so any HTTP client works. The same headers + body apply across languages:
Python (requests)
Node.js (fetch)
Go (raff-go)
Ruby (Net::HTTP)
import os, requests
headers = {
"X-API-Key": os.environ["RAFF_API_KEY"],
"X-Project-ID": os.environ["RAFF_PROJECT_ID"],
"Content-Type": "application/json",
}
resp = requests.get("https://api.rafftechnologies.com/api/v1/vms", headers=headers, timeout=30)
resp.raise_for_status()
for vm in resp.json()["data"]:
print(vm["id"], vm["name"], vm["status"])
const headers = {
"X-API-Key": process.env.RAFF_API_KEY,
"X-Project-ID": process.env.RAFF_PROJECT_ID,
"Content-Type": "application/json",
};
const resp = await fetch("https://api.rafftechnologies.com/api/v1/vms", { headers });
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
const { data } = await resp.json();
for (const vm of data) console.log(vm.id, vm.name, vm.status);
go get github.com/rafftechnologies/raff-go
package main
import (
"context"
"fmt"
"os"
"github.com/rafftechnologies/raff-go"
)
func main() {
client := raff.NewFromToken(
os.Getenv("RAFF_API_KEY"),
raff.SetProjectID(os.Getenv("RAFF_PROJECT_ID")),
)
vms, _, err := client.VMs.List(context.Background(), nil)
if err != nil {
fmt.Fprintln(os.Stderr, "list failed:", err)
os.Exit(1)
}
for _, vm := range vms {
fmt.Println(vm.ID, vm.Name, vm.Status)
}
}
require "net/http"
require "json"
uri = URI("https://api.rafftechnologies.com/api/v1/vms")
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = ENV["RAFF_API_KEY"]
req["X-Project-ID"] = ENV["RAFF_PROJECT_ID"]
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
raise "HTTP #{resp.code}: #{resp.body}" unless resp.code == "200"
JSON.parse(resp.body)["data"].each { |vm| puts "#{vm["id"]} #{vm["name"]} #{vm["status"]}" }