Programmatic access to your entire DartNode infrastructure. Deploy servers, manage networking, control power states, and automate your workflow.
https://api.dartnode.net/v1
All requests and responses use JSON. Include Content-Type: application/json in request bodies and Accept: application/json for all requests.
Get up and running in three steps. This guide walks you through creating an API token and making your first request.
Go to Settings → API Keys in your dashboard. Click Create Token, give it a name, and select the scopes you need. Copy the token - it won't be shown again.
Use the token to fetch your account details:
curl https://api.dartnode.net/v1/account \ -H "Authorization: Bearer dnt_your_token_here" \ -H "Accept: application/json"
import requests
API_TOKEN = "dnt_your_token_here"
BASE_URL = "https://api.dartnode.net/v1"
resp = requests.get(f"{BASE_URL}/account",
headers={"Authorization": f"Bearer {API_TOKEN}"})
data = resp.json()
print(f"Hello, {data['data']['first_name']}!")
const API_TOKEN = "dnt_your_token_here";
const BASE_URL = "https://api.dartnode.net/v1";
const resp = await fetch(`${BASE_URL}/account`, {
headers: { "Authorization": `Bearer ${API_TOKEN}` }
});
const { data } = await resp.json();
console.log(`Hello, ${data.first_name}!`);
$apiToken = "dnt_your_token_here";
$baseUrl = "https://api.dartnode.net/v1";
$ch = curl_init("$baseUrl/account");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiToken"],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
echo "Hello, {$data['data']['first_name']}!";
You're in. Now you can list your instances, control power states, deploy new servers, or set up webhooks for real-time notifications.
All API requests require authentication via a Bearer token in the Authorization header. The API supports two token types.
Scoped tokens tied directly to your account. Best for scripts, automation, and server-to-server integrations. Create them from the dashboard.
Authorization: Bearer dnt_your_token_here
For third-party apps acting on behalf of users. Obtained through the OAuth 2.0 authorization code flow.
Authorization: Bearer dno_access_token_here
All tokens use a prefix to identify their type. This helps with debugging and log scanning.
| Prefix | Type | Usage |
|---|---|---|
| dnt_ | Service Key | Personal API token for direct access |
| dna_ | OAuth client_id | Identifies your OAuth application |
| dns_ | OAuth client_secret | Server-side secret for token exchange |
| dno_ | OAuth access token | Short-lived token for API access (1 hour) |
| dnr_ | OAuth refresh token | Long-lived token to renew access (30 days) |
Tokens are scoped to limit access. When creating a service key or OAuth app, select only the scopes your integration needs.
| Scope | Description |
|---|---|
| account.read | View account profile and settings |
| services.read | List and view services and their details |
| services.power | Start, stop, restart, and reset VMs |
| services.console | Create VNC console sessions |
| services.config.read | View service configuration and OS details |
| services.config.write | Modify hostname, reinstall OS, reset password |
| network.read | List IP addresses and view rDNS records |
| network.rdns.write | Create, update, and clear reverse DNS entries |
| billing.read | View invoices, transactions, and account balance |
| deploy | Deploy new services using account credit |
| deploy.read | List available plans, regions, and OS templates |
| webhooks.read | List and view webhook endpoints |
| webhooks.write | Create, update, and delete webhooks |
Standard request conventions and how to work with paginated results.
Send JSON request bodies with Content-Type: application/json. All responses return JSON with a top-level success boolean:
{
"success": true,
"data": { ... }
}
List endpoints return paginated results. Control pagination with these query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number to retrieve |
| per_page | integer | 25 | Results per page (max 100) |
Paginated responses include metadata to help you navigate:
{
"success": true,
"data": {
"services": [ ... ],
"pagination": {
"page": 1,
"per_page": 25,
"total": 48,
"total_pages": 2
}
}
}
page equals total_pages or the result set is empty.
API requests are rate-limited per token using a sliding window. Check the response headers to monitor your usage.
| X-RateLimit-Limit | Maximum requests per window |
| X-RateLimit-Remaining | Remaining requests in current window |
| X-RateLimit-Reset | Unix timestamp when the window resets |
| Category | Limit | Examples |
|---|---|---|
| Read operations | 120 req/min | GET endpoints (list, detail) |
| Power control | 30 req/min | Start, stop, restart, reset |
| Console sessions | 10 req/min | VNC session creation |
| Write operations | 5 req/min | Deploy, reinstall, config changes |
When rate limited, the API returns 429 Too Many Requests. Use the X-RateLimit-Reset header to know when to retry.
{
"success": false,
"message": "Rate limit exceeded. Try again in 45 seconds."
}
Errors return an appropriate HTTP status code with a JSON body describing what went wrong.
{
"success": false,
"message": "Description of what went wrong"
}
| Code | Meaning | What to Do |
|---|---|---|
| 400 | Bad Request | Check your request body and parameters |
| 401 | Unauthorized | Token is missing, invalid, or expired |
| 402 | Payment Required | Insufficient account credit for the action |
| 403 | Forbidden | Token lacks the required scope for this endpoint |
| 404 | Not Found | Resource doesn't exist or isn't owned by your account |
| 409 | Conflict | Resource state prevents the action (e.g., VM already running) |
| 429 | Too Many Requests | Rate limited - wait and retry |
| 500 | Internal Server Error | Something went wrong on our end - try again or contact support |
success field in the response body, not just the HTTP status code. Some 200 responses may still contain partial errors.
Retrieve your account profile, user details, and current balance.
curl https://api.dartnode.net/v1/account \ -H "Authorization: Bearer dnt_your_token"
resp = requests.get(f"{BASE_URL}/account",
headers={"Authorization": f"Bearer {API_TOKEN}"})
account = resp.json()["data"]
print(f"{account['first_name']} {account['last_name']} ({account['email']})")
const resp = await fetch(`${BASE_URL}/account`, {
headers: { "Authorization": `Bearer ${API_TOKEN}` }
});
const { data } = await resp.json();
console.log(`${data.first_name} ${data.last_name} (${data.email})`);
$ch = curl_init("$baseUrl/account");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiToken"],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true)["data"];
echo "{$data['first_name']} {$data['last_name']} ({$data['email']})";
{
"success": true,
"data": {
"id": 1,
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"profile_name": "JohnDoe",
"is_verified": true,
"two_factor_enabled": false,
"created_at": "2024-01-15T10:30:00+00:00",
"account": {
"id": 1,
"name": "John Doe",
"country": "US",
"state": "Texas",
"city": "Dallas"
},
"urls": {
"compute": "https://api.dartnode.net/v1/compute",
"billing_balance": "https://api.dartnode.net/v1/billing/balance",
"invoices": "https://api.dartnode.net/v1/billing/invoices",
"network_ips": "https://api.dartnode.net/v1/network/ips",
"webhooks": "https://api.dartnode.net/v1/webhooks",
"deploy": "https://api.dartnode.net/v1/deploy"
}
}
}
urls object with HATEOAS-style links to related resources, making it easy to discover available API endpoints.
Manage your cloud compute instances (virtual machines). List, inspect, control power states, access consoles, and modify configuration.
Returns a paginated list of all virtual machines on your account. Each instance includes its current power state, IP addresses, resource specs, and real-time usage metrics.
| Parameter | Type | Description |
|---|---|---|
| page | integer optional | Page number (default: 1) |
| per_page | integer optional | Results per page, max 100 (default: 25) |
| status | string optional | Filter by status: active, suspended, pending, terminated |
curl "https://api.dartnode.net/v1/compute?status=active&per_page=10" \ -H "Authorization: Bearer dnt_your_token"
resp = requests.get(f"{BASE_URL}/compute",
headers={"Authorization": f"Bearer {API_TOKEN}"},
params={"status": "active", "per_page": 10})
for vm in resp.json()["data"]["services"]:
print(f"{vm['id']}: {vm['hostname']} ({vm['status']})")
const resp = await fetch(`${BASE_URL}/compute?status=active&per_page=10`, {
headers: { "Authorization": `Bearer ${API_TOKEN}` }
});
const { data } = await resp.json();
data.services.forEach(vm =>
console.log(`${vm.id}: ${vm.hostname} (${vm.status})`));
$ch = curl_init("$baseUrl/compute?status=active&per_page=10");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiToken"],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true)["data"];
foreach ($data["services"] as $vm) {
echo "{$vm['id']}: {$vm['hostname']} ({$vm['status']})\n";
}
{
"success": true,
"data": {
"services": [
{
"id": 123,
"url": "https://api.dartnode.net/v1/compute/123",
"hostname": "web-server-1",
"status": "running",
"service_status": "active",
"primary_ip": "192.0.2.10",
"ips": [
{
"address": "192.0.2.10",
"type": "ipv4",
"ptr": "web.example.com",
"is_primary": true,
"has_ddos_protection": false,
"url": "https://api.dartnode.net/v1/network/ips/192.0.2.10",
"rdns_url": "https://api.dartnode.net/v1/network/ips/192.0.2.10/rdns"
}
],
"cpu": {
"cores": 2,
"usage_percent": 12.5
},
"memory": {
"total_mb": 4096,
"used_mb": 1024,
"usage_percent": 25.0
},
"disk_gb": 80,
"region": "us-dal",
"created_at": "2024-06-10T14:00:00+00:00"
}
],
"pagination": {
"page": 1,
"per_page": 10,
"total": 3,
"total_pages": 1
}
}
}
| Field | Type | Description |
|---|---|---|
| id | integer | Unique instance identifier |
| url | string | API URL to fetch full instance details |
| hostname | string | Instance hostname |
| status | string | VM power state: running, stopped, provisioning, unknown |
| service_status | string | Billing lifecycle: active, suspended, terminated |
| primary_ip | string | Primary IPv4 address |
| ips | array | All assigned IP addresses with rDNS and DDoS info |
| cpu | object | CPU core count and real-time usage percentage |
| memory | object | Total, used, and percentage memory utilization |
| disk_gb | integer | Disk allocation in GB |
| region | string | Compute region code |
Returns full details for a single compute instance including current resource usage, IP addresses, OS information, billing, and HATEOAS links to related actions.
| Parameter | Type | Description |
|---|---|---|
| sid | integer required | Instance (service) ID |
curl https://api.dartnode.net/v1/compute/123 \ -H "Authorization: Bearer dnt_your_token"
resp = requests.get(f"{BASE_URL}/compute/123",
headers={"Authorization": f"Bearer {API_TOKEN}"})
vm = resp.json()["data"]
print(f"{vm['hostname']} - {vm['os']} ({vm['status']})")
print(f"CPU: {vm['cpu']['cores']} cores @ {vm['cpu']['usage_percent']}%")
print(f"RAM: {vm['memory']['used_mb']}MB / {vm['memory']['total_mb']}MB")
const resp = await fetch(`${BASE_URL}/compute/123`, {
headers: { "Authorization": `Bearer ${API_TOKEN}` }
});
const { data: vm } = await resp.json();
console.log(`${vm.hostname} - ${vm.os} (${vm.status})`);
console.log(`CPU: ${vm.cpu.cores} cores @ ${vm.cpu.usage_percent}%`);
console.log(`RAM: ${vm.memory.used_mb}MB / ${vm.memory.total_mb}MB`);
$ch = curl_init("$baseUrl/compute/123");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiToken"],
CURLOPT_RETURNTRANSFER => true,
]);
$vm = json_decode(curl_exec($ch), true)["data"];
echo "{$vm['hostname']} - {$vm['os']} ({$vm['status']})";
{
"success": true,
"data": {
"id": 123,
"url": "https://api.dartnode.net/v1/compute/123",
"hostname": "web-server-1",
"status": "running",
"service_status": "active",
"ips": [
{
"address": "192.0.2.10",
"type": "ipv4",
"ptr": "web.example.com",
"is_primary": true,
"has_ddos_protection": false,
"url": "https://api.dartnode.net/v1/network/ips/192.0.2.10",
"rdns_url": "https://api.dartnode.net/v1/network/ips/192.0.2.10/rdns"
}
],
"cpu": {
"cores": 2,
"usage_percent": 12.5
},
"memory": {
"total_mb": 4096,
"used_mb": 1024,
"usage_percent": 25.0
},
"disk_gb": 80,
"os": "Ubuntu 24.04",
"region": "us-dal",
"billing": {
"amount": 9.95,
"term_months": 1,
"next_payment": "2026-04-10"
},
"created_at": "2024-06-10T14:00:00+00:00",
"urls": {
"power": "https://api.dartnode.net/v1/compute/123/power",
"console_vnc": "https://api.dartnode.net/v1/compute/123/console/vnc",
"config": "https://api.dartnode.net/v1/compute/123/config"
}
}
}
status field reflects the VM power state (running, stopped, provisioning, unknown), while service_status reflects the billing lifecycle (active, suspended, terminated). A VM can be stopped but still active in billing.
Start, stop, gracefully shutdown, or hard reset your compute instances.
| Parameter | Type | Description |
|---|---|---|
| action | string required | Power action to perform |
| Action | Description |
|---|---|
| on | Power on the instance (boot from off state) |
| off | Immediate power off (like pulling the plug - data loss possible) |
| shutdown | Graceful ACPI shutdown signal (recommended) |
| reset | Hard reset (power cycle without graceful shutdown) |
shutdown instead of off whenever possible. The off action is equivalent to pulling the power cable and may cause filesystem corruption on the guest OS.
curl -X POST https://api.dartnode.net/v1/compute/123/power \
-H "Authorization: Bearer dnt_your_token" \
-H "Content-Type: application/json" \
-d '{"action": "shutdown"}'
resp = requests.post(f"{BASE_URL}/compute/123/power",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={"action": "shutdown"})
print(resp.json()["data"]["message"])
const resp = await fetch(`${BASE_URL}/compute/123/power`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ action: "shutdown" })
});
console.log((await resp.json()).data.message);
$ch = curl_init("$baseUrl/compute/123/power");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $apiToken",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode(["action" => "shutdown"]),
CURLOPT_RETURNTRANSFER => true,
]);
echo json_decode(curl_exec($ch), true)["data"]["message"];
{
"success": true,
"data": {
"action": "shutdown",
"message": "Shutdown signal sent"
}
}
Get direct graphical console access to your compute instances via a WebSocket-based VNC session. Useful for recovery, debugging, or when SSH is unreachable.
Returns a WebSocket URL with a time-limited token. Connect to the URL promptly to open the session.
{
"success": true,
"data": {
"url": "wss://node1.dartnode.net:8443/vnc/vm-123?token=abc123",
"token": "abc123"
}
}
View and modify instance configuration. Change hostnames, reinstall the operating system, and reset root passwords.
{
"success": true,
"data": {
"hostname": "web-server-1",
"os": "Ubuntu 24.04",
"os_type": "linux",
"cpu": 2,
"memory_mb": 4096,
"disk_gb": 80,
"has_root_password": true
}
}
| Parameter | Type | Description |
|---|---|---|
| hostname | string required | New hostname (1-255 chars, alphanumeric with dots/dashes) |
| Parameter | Type | Description |
|---|---|---|
| os | integer required | OS template ID (from /v1/deploy/os-templates) |
{
"success": true,
"data": {
"job_id": "x1y2z3",
"message": "OS reinstall queued"
}
}
Queues a root password reset job. The new password will be delivered via email once the operation completes.
{
"success": true,
"data": {
"job_id": "a1b2c3d4",
"message": "Root password reset queued"
}
}
GET /v1/compute/{sid} to check when the operation completes.
Manage IP addresses assigned to your services and configure reverse DNS (rDNS) records for email deliverability and service identification.
{
"success": true,
"data": {
"ips": [
{
"address": "192.0.2.10",
"type": "ipv4",
"ptr": "web.example.com",
"resolved_ptr": "web.example.com",
"has_ddos_protection": false,
"attached_to": {
"id": 123,
"hostname": "web-server-1"
},
"url": "https://api.dartnode.net/v1/network/ips/192.0.2.10",
"rdns_url": "https://api.dartnode.net/v1/network/ips/192.0.2.10/rdns"
}
],
"total": 1
}
}
| Parameter | Type | Description |
|---|---|---|
| value | string required | Fully qualified domain name (e.g. mail.example.com) |
curl -X PUT https://api.dartnode.net/v1/network/ips/192.0.2.1/rdns \
-H "Authorization: Bearer dnt_your_token" \
-H "Content-Type: application/json" \
-d '{"value": "mail.example.com"}'
resp = requests.put(f"{BASE_URL}/network/ips/192.0.2.1/rdns",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={"value": "mail.example.com"})
print(resp.json()["data"]["message"])
await fetch(`${BASE_URL}/network/ips/192.0.2.1/rdns`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${API_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ value: "mail.example.com" })
});
$ch = curl_init("$baseUrl/network/ips/192.0.2.1/rdns");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $apiToken",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode(["value" => "mail.example.com"]),
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);
Removes the rDNS record for the specified IP address, reverting it to the default ISP hostname.
Access your invoices, view transaction history, and check your account balance.
| Parameter | Type | Description |
|---|---|---|
| page | integer optional | Page number (default: 1) |
| per_page | integer optional | Results per page (default: 25) |
| status | string optional | Filter: paid, unpaid, overdue, refunded |
{
"success": true,
"data": {
"invoices": [
{
"id": 789,
"url": "https://api.dartnode.net/v1/billing/invoices/789",
"description": "Cloud VPS 4GB - Monthly",
"status": "paid",
"total": 9.95,
"credit_applied": 0.00,
"due_at": "2026-03-01T00:00:00+00:00",
"paid_at": "2026-02-28T12:00:00+00:00",
"created_at": "2026-02-15T10:00:00+00:00"
}
],
"pagination": { "page": 1, "per_page": 25, "total": 12, "total_pages": 1 }
}
}
{
"success": true,
"data": {
"id": 789,
"url": "https://api.dartnode.net/v1/billing/invoices/789",
"description": "Cloud VPS 4GB - Monthly",
"status": "paid",
"total": 9.95,
"credit_applied": 0.00,
"due_at": "2026-03-01T00:00:00+00:00",
"paid_at": "2026-02-28T12:00:00+00:00",
"created_at": "2026-02-15T10:00:00+00:00",
"items": [
{
"id": 1,
"description": "Cloud VPS 4GB - Monthly",
"amount": 9.95,
"quantity": 1,
"total": 9.95
}
]
}
}
{
"success": true,
"data": {
"balance": -25.50,
"currency": "USD"
}
}
balance field is negative when you have credit. A balance of -25.50 means you have $25.50 of credit available.
Provision new virtual machines using your account credit. The deploy flow lets you choose a plan, region, and OS, then launch a server in seconds.
Pass the same parameters as the deploy request to get a price breakdown before committing.
| Parameter | Type | Description |
|---|---|---|
| plan_id | integer required | Plan ID from /v1/deploy/plans |
| region | string required | Region code from /v1/deploy/regions |
| hostname | string required | Server hostname |
| os | integer required | OS category ID |
| os_version | integer required | OS template ID |
| ipv4 | integer optional | Number of IPv4 addresses (default: 1) |
| ipv6 | integer optional | Number of IPv6 addresses (default: 1) |
| ddos | boolean optional | Enable DDoS protection ($2.95/mo) |
| backups | boolean optional | Enable cloud backups ($4.95/mo) |
| cycle | integer optional | Billing cycle in months: 1, 3, 6, or 12 (default: 1) |
curl -X POST https://api.dartnode.net/v1/deploy \
-H "Authorization: Bearer dnt_your_token" \
-H "Content-Type: application/json" \
-d '{
"plan_id": 1,
"region": "us-dal",
"hostname": "my-server",
"os": 1,
"os_version": 5,
"ddos": false,
"backups": true,
"cycle": 1
}'
resp = requests.post(f"{BASE_URL}/deploy",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={
"plan_id": 1,
"region": "us-dal",
"hostname": "my-server",
"os": 1,
"os_version": 5,
"backups": True,
"cycle": 1
})
deploy = resp.json()["data"]["deploy"]
print(f"Service #{deploy['service_id']} - {deploy['status']}")
const resp = await fetch(`${BASE_URL}/deploy`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
plan_id: 1,
region: "us-dal",
hostname: "my-server",
os: 1,
os_version: 5,
backups: true,
cycle: 1
})
});
const { data } = await resp.json();
console.log(`Service #${data.deploy.service_id} - ${data.deploy.status}`);
$ch = curl_init("$baseUrl/deploy");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $apiToken",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode([
"plan_id" => 1,
"region" => "us-dal",
"hostname" => "my-server",
"os" => 1,
"os_version" => 5,
"backups" => true,
"cycle" => 1
]),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true)["data"];
echo "Service #{$data['deploy']['service_id']}";
{
"success": true,
"data": {
"message": "VM deployment initiated successfully. The server will be provisioned shortly.",
"deploy": {
"order_id": "abc123",
"service_id": 456,
"service_url": "https://api.dartnode.net/v1/compute/456",
"invoice_id": 790,
"invoice_url": "https://api.dartnode.net/v1/billing/invoices/790",
"status": "provisioning",
"amount_charged": 9.95,
"recurring_amount": 9.95,
"billing_cycle_months": 1,
"next_due_date": "2026-04-05",
"hostname": "my-server",
"plan": {
"id": 1,
"name": "Cloud VPS 2GB",
"vcpu": 1,
"memory_mb": 2048,
"storage_gb": 40
},
"region": "us-dal",
"os": {
"category": "Ubuntu",
"template": "Ubuntu 24.04"
}
}
}
}
GET /v1/compute/{sid} to poll the status until it changes from provisioning to active. You can also set up a webhook to get notified when the server is ready.
Create and manage webhook endpoints to receive real-time HTTP notifications when events occur on your account.
| Parameter | Type | Description |
|---|---|---|
| name | string required | Display name for the webhook |
| url | string required | HTTPS endpoint URL that will receive events |
| events | array required | Event types to subscribe to, or ["*"] for all events |
curl -X POST https://api.dartnode.net/v1/webhooks \
-H "Authorization: Bearer dnt_your_token" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Alerts",
"url": "https://yourapp.com/webhooks/dartnode",
"events": ["service.provisioned", "service.suspended", "invoice.paid"]
}'
resp = requests.post(f"{BASE_URL}/webhooks",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={
"name": "Production Alerts",
"url": "https://yourapp.com/webhooks/dartnode",
"events": ["service.provisioned", "service.suspended", "invoice.paid"]
})
webhook = resp.json()["data"]
print(f"Webhook #{webhook['webhook']['id']} created with secret: {webhook['webhook']['secret']}")
const resp = await fetch(`${BASE_URL}/webhooks`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Production Alerts",
url: "https://yourapp.com/webhooks/dartnode",
events: ["service.provisioned", "service.suspended", "invoice.paid"]
})
});
const { data } = await resp.json();
console.log(`Webhook #${data.webhook.id} created with secret: ${data.webhook.secret}`);
secret. It is returned only when the webhook is created. You'll need it to verify signatures on incoming deliveries.
Subscribe to specific events to receive targeted notifications. Use * to receive all events.
| Event Type | Description |
|---|---|
| service.provisioned | A new service has been provisioned and is ready to use |
| service.suspended | A service has been suspended (usually for non-payment) |
| service.unsuspended | A suspended service has been reactivated |
| service.terminated | A service has been permanently terminated and data deleted |
| service.cancelled | A cancellation request has been submitted for a service |
| service.deployed | A new VM deployment has been initiated via the API |
| Event Type | Description |
|---|---|
| service.power.on | A VM has been powered on |
| service.power.off | A VM has been force stopped |
| service.power.shutdown | A graceful shutdown has been initiated on a VM |
| service.power.reboot | A VM has been rebooted |
| Event Type | Description |
|---|---|
| invoice.created | A new invoice has been generated |
| invoice.paid | An invoice has been successfully paid |
| invoice.past_due | An invoice is past its due date |
| refund.issued | A refund has been processed to the original payment method |
| Event Type | Description |
|---|---|
| ip.attached | An IP address was attached to a service |
| ip.detached | An IP address was detached from a service |
Every webhook delivery includes a cryptographic signature so you can verify it originated from DartNode and hasn't been tampered with.
| X-DartNode-Signature | HMAC-SHA256 signature of the payload |
| X-DartNode-Timestamp | Unix timestamp used in signing |
| X-DartNode-Event | The event type (e.g. service.provisioned) |
v1=HMAC-SHA256(timestamp.payload, secret_hash)
The signed payload is the concatenation of the timestamp, a dot (.), and the raw request body.
import hmac, hashlib, time
def verify_webhook(payload: bytes, signature: str,
timestamp: str, secret_hash: str) -> bool:
# Reject deliveries older than 5 minutes (replay protection)
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.".encode() + payload
expected = "v1=" + hmac.new(
secret_hash.encode(), signed_payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Usage in Flask:
@app.route("/webhooks/dartnode", methods=["POST"])
def handle_webhook():
if not verify_webhook(
request.data,
request.headers["X-DartNode-Signature"],
request.headers["X-DartNode-Timestamp"],
WEBHOOK_SECRET
):
return "Invalid signature", 401
event = request.headers["X-DartNode-Event"]
data = request.json
# Process event...
return "OK", 200
const crypto = require("crypto");
function verifyWebhook(payload, signature, timestamp, secretHash) {
// Reject deliveries older than 5 minutes
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return false;
}
const signedPayload = `${timestamp}.${payload}`;
const expected = "v1=" + crypto
.createHmac("sha256", secretHash)
.update(signedPayload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(signature)
);
}
// Usage in Express:
app.post("/webhooks/dartnode", express.raw({ type: "*/*" }), (req, res) => {
const valid = verifyWebhook(
req.body.toString(),
req.headers["x-dartnode-signature"],
req.headers["x-dartnode-timestamp"],
WEBHOOK_SECRET
);
if (!valid) return res.status(401).send("Invalid signature");
const event = req.headers["x-dartnode-event"];
const data = JSON.parse(req.body);
// Process event...
res.sendStatus(200);
});
function verifyWebhook(
string $payload, string $signature,
string $timestamp, string $secretHash
): bool {
// Reject deliveries older than 5 minutes
if (abs(time() - (int) $timestamp) > 300) {
return false;
}
$signedPayload = $timestamp . '.' . $payload;
$expected = 'v1=' . hash_hmac('sha256', $signedPayload, $secretHash);
return hash_equals($expected, $signature);
}
// Usage:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_DARTNODE_SIGNATURE'];
$timestamp = $_SERVER['HTTP_X_DARTNODE_TIMESTAMP'];
if (!verifyWebhook($payload, $signature, $timestamp, WEBHOOK_SECRET)) {
http_response_code(401);
exit('Invalid signature');
}
$event = $_SERVER['HTTP_X_DARTNODE_EVENT'];
$data = json_decode($payload, true);
// Process event...
http_response_code(200);
All webhook deliveries are HTTP POST requests with a JSON body. DartNode waits up to 10 seconds for a 2xx response.
| Content-Type | application/json |
| User-Agent | DartNode-Webhooks/1.0 |
| X-DartNode-Event | The event type (e.g. service.provisioned) |
| X-DartNode-Signature | HMAC-SHA256 signature for verification |
| X-DartNode-Timestamp | Unix timestamp used in signing |
{
"id": "evt_a1b2c3d4e5f6",
"type": "service.provisioned",
"created_at": "2026-03-02T15:30:00+00:00",
"data": {
"service_id": 456,
"service_type": "vps",
"status": "active",
"hostname": "my-server",
"plan": 1,
"region": "us-dal"
}
}
{
"id": "evt_f6e5d4c3b2a1",
"type": "invoice.paid",
"created_at": "2026-03-02T15:30:00+00:00",
"data": {
"invoice_id": 789,
"status": "paid",
"total": 9.95,
"balance": 0,
"items": [
{
"description": "Base Plan: Cloud VPS 2GB",
"amount": 9.95,
"quantity": 1
}
],
"customer_id": 123
}
}
Build third-party applications that access DartNode on behalf of users using the OAuth 2.0 Authorization Code Grant flow.
If you only need to access your own account, use Service Keys instead - they're simpler and don't require the OAuth flow.
Create your OAuth application from the dashboard. You'll receive a client_id (prefixed dna_) and client_secret (prefixed dns_).
client_secret secure. Never expose it in client-side code, public repositories, or browser-accessible files. The token exchange must happen server-side.
The standard OAuth 2.0 Authorization Code flow in three steps.
Redirect the user to the DartNode OAuth authorization page with your app details:
https://oauth.dartnode.com/authorize ?client_id=dna_your_client_id &redirect_uri=https://yourapp.com/callback &scope=services.read services.power &response_type=code &state=random_csrf_token
| Parameter | Required | Description |
|---|---|---|
| client_id | Yes | Your OAuth app's client ID (dna_ prefix) |
| redirect_uri | Yes | Must exactly match one of your registered redirect URIs |
| response_type | Yes | Must be code |
| scope | No | Space-separated list of requested scopes (defaults to app's registered scopes) |
| state | Recommended | Random string for CSRF protection (verify on callback) |
https://api.dartnode.net/oauth/authorize as an alternative entry point - it validates the request and redirects to the OAuth consent domain automatically.
The user is taken to oauth.dartnode.com where they will:
If the user approves, they are redirected back to your redirect_uri with an authorization code:
https://yourapp.com/callback?code=dnc_xxx&state=random_csrf_token
If the user denies the request:
https://yourapp.com/callback?error=access_denied&error_description=The+user+denied+the+request&state=random_csrf_token
state parameter matches what you sent in Step 1. This prevents CSRF attacks where an attacker tricks a user into authorizing a malicious request.
dnc_ prefix) expire after 10 minutes and are single-use. Exchange them for tokens immediately after receiving the callback.
Exchange the authorization code for access and refresh tokens. This request must be made server-side.
curl -X POST https://api.dartnode.net/v1/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=dnc_xxx" \ -d "client_id=dna_your_client_id" \ -d "client_secret=dns_your_client_secret" \ -d "redirect_uri=https://yourapp.com/callback"
resp = requests.post("https://api.dartnode.net/v1/oauth/token", data={
"grant_type": "authorization_code",
"code": "dnc_xxx",
"client_id": "dna_your_client_id",
"client_secret": "dns_your_client_secret",
"redirect_uri": "https://yourapp.com/callback"
})
tokens = resp.json()["data"]
access_token = tokens["access_token"] # dno_xxx (1 hour)
refresh_token = tokens["refresh_token"] # dnr_xxx (30 days)
const resp = await fetch("https://api.dartnode.net/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: "dnc_xxx",
client_id: "dna_your_client_id",
client_secret: "dns_your_client_secret",
redirect_uri: "https://yourapp.com/callback"
})
});
const { data } = await resp.json();
const { access_token, refresh_token } = data;
$ch = curl_init("https://api.dartnode.net/v1/oauth/token");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
"grant_type" => "authorization_code",
"code" => "dnc_xxx",
"client_id" => "dna_your_client_id",
"client_secret" => "dns_your_client_secret",
"redirect_uri" => "https://yourapp.com/callback"
]),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true)["data"];
$accessToken = $data["access_token"];
$refreshToken = $data["refresh_token"];
{
"success": true,
"data": {
"access_token": "dno_xxx",
"refresh_token": "dnr_xxx",
"expires_in": 3600,
"token_type": "Bearer"
}
}
Access tokens expire after 1 hour. Use refresh tokens to obtain new access tokens without requiring the user to re-authorize.
Refresh tokens are valid for 30 days. Each refresh returns a new access token and a new refresh token (the old refresh token is invalidated).
curl -X POST https://api.dartnode.net/v1/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=dnr_xxx" \ -d "client_id=dna_your_client_id" \ -d "client_secret=dns_your_client_secret"
| Token Type | Lifetime | Renewal |
|---|---|---|
| Access Token | 1 hour | Use refresh token to get a new one |
| Refresh Token | 30 days | New one issued with each refresh |
| Authorization Code | 10 minutes | Single use - exchange immediately |