If your last request came back with 401 Unauthorized and you're sure you copied the token correctly, the problem is probably somewhere else: you sent the Authorization header with the wrong prefix, or you're using the token from an environment whose IP isn't in the allowlist. This text is written for someone who knows what they want and just needs the exact value and official behavior.
Token authentication: what actually goes in the header
No cookies, no session, no HMAC signature. Every request has one header and that's it:
Authorization: Bearer snc_live_9f3a...c41d
Content-Type: application/json
The Bearer prefix is case-sensitive. bearer in lowercase returns 401 and the response body says nothing to tell you the problem is the casing. I've seen this a lot in support logs.
There are two types of tokens: snc_live_ for real operations on resources, and snc_test_ which only works against the sandbox environment and returns 403 on real resources. Put the test token in CI, not on a production server.
Every token has a scope. A token with only compute:read gets 403 Forbidden for POST /v1/instances, not 401. The difference between these two codes matters: 401 means I didn't recognize your identity, 403 means I recognized it but you're not allowed. If your monitoring treats these two as the same, you'll generate useless alerts.
Token rotation without service interruption
Each account can have two active tokens at the same time. The correct order is: create a new token, bring the service up with it, shift traffic, then revoke the old token. If you revoke first, in-flight requests will fail with 401 and if you don't have retry logic, you'll see an error window in your graph.
A revoked token becomes invalid immediately; there's no server-side cache. Keep this in mind when designing your retry logic.
Endpoints: what's under /v1
Everything is versioned. A path without /v1 doesn't exist and returns 404, not a redirect. This is intentional.
| Method and path | Purpose | Note |
|---|---|---|
GET /v1/instances | List servers | Pagination with ?page= and ?per_page= (max 100) |
POST /v1/instances | Create server | Response is 202, not 201 |
GET /v1/instances/{id} | Status of one server | The status field stays on building until provisioning completes |
POST /v1/instances/{id}/actions | Power on/off/restart | Body: {"type":"reboot"} |
GET /v1/volumes | Detached disks | NVMe only in current plans |
GET /v1/snapshots | Snapshots | Creating a snapshot from a running server is allowed |
GET /v1/regions | Available regions | Responds even without authentication |
The point that wastes people's time: POST /v1/instances returns 202 Accepted and its body has the id, but the server doesn't actually exist yet. If you immediately issue a GET, you might get 404. You need to poll until status goes from building to running. The typical window is between 30 and 90 seconds and depends on the region and plan size.
For heavier infrastructure work, like bringing up a cluster or connecting a private network between a database and web, this API only builds the shell; the rest is explained in Private networking between servers; secure database and web communication without the internet.
Rate limits: the numbers and the headers
The default cap is 60 requests per minute per token, and 600 requests per minute per IP. Which one hits first? Whichever reaches its cap sooner. If you split one token across ten services, each effectively gets six requests per minute, and this is where many people get caught off guard.
Every response has these headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1718032440
Retry-After: 12
X-RateLimit-Reset is a Unix timestamp, not a number of seconds. If you read it as "seconds until reset," your backoff logic becomes meaningless. Retry-After only appears on 429 responses and is what you should use directly.
Write operations have a separate and stricter cap: 20 requests per minute. The reason is clear; creating a server is a heavy operation on the hypervisor. If you have a provisioning script that creates 50 servers back-to-back, it will fall over with 429. The right approach is queueing with a minimum of three seconds between write requests, not increasing parallelism.
This is where they go wrong
The common mistake I see in tickets: someone gets a 429, puts the retry in a loop with zero delay, and then complains that "the API is down." What we see in the logs on our side is a burst of concurrent requests that keeps the rate window constantly full and effectively never lets you out of the cap. The sign is this: the error graph is flat and continuous, not spiky. If the error is continuous, the problem is rate, not the network.
The solution: exponential backoff with jitter, and respect Retry-After. Write it once in code, use it everywhere.
Error response format: what you actually parse
All errors, without exception, have one shape:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many write requests. Retry after 12 seconds.",
"request_id": "req_8c21f0a9b7e4",
"details": [
{ "field": "region", "issue": "unavailable" }
]
}
}
Branch on code, not on message. The message text may change; the code is stable. Store request_id in your own logs. Without it, when you open a ticket, we can't find your specific request and the conversation drags on for two days.
Validation errors return 422, not 400. The details array tells you exactly which field has the problem. If your parser only reads message, you're throwing this information away.
Which code to expect where
400— malformed JSON body or wrongContent-Type.401— token missing, expired, or wrongBearerprefix.403— token is valid but doesn't have enough scope.404— resource doesn't exist, or the path version (/v1) is missing.409— state conflict; for example, restarting a server that's shutting down.422— field validation rejected.429— rate limit.5xx— our side. Retry with backoff is allowed.
A subtle trap: don't retry 409. If the server is in a transitional state, repeating the request makes things worse. You should poll the status until the transition finishes.
Limits you'll hit in practice
The API is designed for automation, not real-time monitoring. If you want to poll the status of 200 servers every 5 seconds, don't do it; instead set up a webhook or monitoring on the server side itself. The guide Cloud monitoring; a practical guide to monitoring and alerting in cloud infrastructure is written exactly for this case and doesn't exceed the API rate limit.
Second limit: pagination. GET /v1/instances returns a maximum of 100 items per page and has no "give me everything" filter. If you have more than 100 resources, you need to loop. The default order is by creation time descending, so new resources are always on the first page.
Third and more important limit: delete operations are irreversible. DELETE /v1/instances/{id} executes immediately and the disk is wiped too. If you haven't taken a snapshot before risky changes, there's nothing to restore. Read how to work with Snapshots in the cloud; a lifesaver before risky changes before your first delete script, not after.
Which approach to choose: raw API or official tooling
If you only have a few servers and your tasks are repetitive, choose the CLI. You'll get results faster and won't have to write token management and backoff yourself. If provisioning is part of a pipeline, or you need to coordinate across dozens of resources, choose the raw API; but take the retry and queueing layer seriously from day one.
The case where the choice is clearly wrong: using the API for tasks that happen once a month. Writing and maintaining code for these costs more than three clicks in the panel.
For workloads that scale themselves, instead of writing scaling logic on top of the API, use Managed Kubernetes and let it handle scaling. If you just want to scale resources up and down without downtime, Scaling cloud resources without downtime; a practical guide to upgrading infrastructure gives a more precise path. And if you're building your entire infrastructure from scratch, Cloud infrastructure (IaaS) is a more sensible starting point than building everything on the raw API.
Frequently asked questions
Why do I get a 401 response despite having the correct token?
There are three common causes. First, the Bearer prefix is written in lowercase. Second, an snc_test_ token is used on a real resource, whose correct response is 403 but some interfaces show it as 401. Third, the token has been revoked and there's no server-side cache, so it's invalid immediately.
If you've ruled out all three, grab the response's request_id and follow up with it. Without this identifier, finding your specific request is practically impossible.
How much is the ServerNet API rate limit and how do I get around it?
The default cap is 60 requests per minute per token and 600 requests per minute per IP. Write operations have a separate cap of 20 requests per minute. There's no way around it, and trying to get around it keeps the window full and makes things worse.
What you should do: read the Retry-After header directly, leave at least three seconds between write requests, and don't split a token across services. For continuous monitoring, use webhooks instead of polling the API.
Is deleting a server via the API reversible?
No. DELETE /v1/instances/{id} executes immediately and the server's disk is wiped too. There's no trash bin or retention period.
Take a snapshot before any delete script. Creating a snapshot from a running server is allowed, and this alone is the difference between a ten-minute incident and a lost day.
Why is the create server response 202 and not 201?
Because creating a server is an asynchronous operation. A 202 response means the request was accepted, not that the resource was created. The body includes the id but the server is still in the building state.
You need to poll the status with GET /v1/instances/{id} until it reaches running. This window is typically between 30 and 90 seconds. If you issue a GET request immediately after 202, you might get 404, and that's normal.