Getting started
API conventions
The rules every Serverside.com API endpoint shares: base URL, naming, money units, what a success and a failure look like, pagination, error codes, retries and idempotency.
Base URL and versions
https://api.serverside.com
Every path starts with a version segment, /v1. Requests and responses are JSON over HTTPS; send Content-Type: application/json with any request that has a body. Every response carries an X-Request-Id header.
Names and values
| Where | Style | Example |
|---|---|---|
| Query parameters | camelCase | ?pageSize=50&sortColumn=name |
| JSON request and response fields | camelCase | "plan", "primaryIpv4" |
| Allowed values | UPPER_SNAKE_CASE strings | "GRACEFUL_RESTART", "RAID10" |
A field with a fixed set of values lists them under Allowed values on its endpoint page. Every set is upper case except an error detail's location, which only appears in error responses. The API matches them without regard to case, so semi_annually reads as SEMI_ANNUALLY. Another spelling (SemiAnnually), a number in place of the name or a comma-separated list is refused with 400, and the error names the values the field takes. The one exception is the type of an order item, which must be written exactly: BAREMETAL or IP_BLOCK.
IDs are UUIDs, and a field with no id to give is null, never an empty string. A few fields that take an id also take a name that never changes: a bare metal plan's slug (epyc-9354p) and a datacenter's code, three letters for the metro and a site number (ams1). True-or-false fields are named as questions starting with is, such as isLocked, and a field that carries a unit names it: speedMbps, memoryGb, sizeBytes. A duration you send is in seconds.
IP addresses and networks travel as strings, such as "192.0.2.10" and "192.0.2.0/28". An IPv4 address must be written in its usual dotted form, so 10.1 and 010.0.0.1 are refused; an IPv6 address may be written in any form but without brackets or a zone. Timestamps are ISO 8601 strings. One you send has to carry its offset: 2026-09-20T00:00:00Z and 2026-09-20T02:00:00+02:00 are read as the same instant, and 2026-09-20T00:00:00 is refused. Every timestamp the API sends is in UTC and ends in Z.
Request bodies
A JSON property the endpoint does not declare is refused with 400 VALIDATION_ERROR, and the INVALID_BODY detail names the property, so a misspelled optional field is caught rather than dropped.
PATCH changes only what the body names: a field you leave out keeps its value, and the answer is 200 with the resource as its GET returns it. The three PATCH operations, on a server, a virtual network and an SSH key, take one field, name, and a null name leaves the name as it was. PUT replaces one setting whole, such as a server's boot mode or its iPXE script; lock and unlock are a PUT pair as well.
Money
Amounts are whole numbers in the currency of their record, which carries it as currency (pricing.currency on a plan). Whole numbers keep every amount exact. Most JSON parsers read a decimal such as 0.10 as a binary floating-point number, which cannot hold it exactly: in JavaScript, 0.1 + 0.2 is 0.30000000000000004. The field name gives the scale:
- A field whose name ends in
Millicentscounts thousandths of a cent:150000is 1.50. Prices and rates are written this way, on plans, orders and subscriptions, so an hourly rate keeps its fraction of a cent. - Every other money field counts cents:
12999is 129.99. Invoices, payments, usage charges and account credit are written this way.
An order carries only its price. Tax, discounts and the amount due are on the invoice (Billing).
Success and error responses
The HTTP status tells the two shapes apart: a 2xx answer is a success, anything else is an error, and success repeats that in the body.
Success
{
"success": true,
"data": { },
"pagination": {
"totalRecords": 42,
"page": 1,
"pageSize": 25,
"totalPages": 2
}
}
| Field | Meaning |
|---|---|
success | true. |
data | The payload: an object, or an array on list endpoints. |
pagination | On paged lists (Pagination). Every other response leaves it out. |
A call that creates something answers 201 with what it created, in the shape a read of it returns. 202 Accepted means the change is still being applied. Attaching a server to a virtual network, detaching it and switching its boot mode put the change's progress in data: a status of APPLYING and since, the time the change was accepted. Deleting a server answers 202 with no body while the server is torn down. 204 No Content answers have no body at all. Download invoice PDF is the one endpoint that returns a file instead of JSON.
Error
{
"success": false,
"data": null,
"code": "VALIDATION_ERROR",
"message": "pageSize: Must be between 1 and 1000.",
"traceId": "SRV-7F3A-0C21-9B4E-51D2",
"details": [
{
"code": "INVALID_VALUE",
"message": "Must be between 1 and 1000.",
"field": "pageSize",
"location": "query"
}
]
}
| Field | Meaning |
|---|---|
success | false. |
data | null, except when an invoice payment is refused: then it holds the payment result (Billing). |
code | What went wrong, as a stable UPPER_SNAKE_CASE code. Branch on it together with the status. A code your client does not know yet is handled by its status. |
message | A sentence for people. Its wording can change, so don't parse it. |
traceId | The request's id, equal to the X-Request-Id header. Quote it when you contact support. |
details | One entry per problem, on validation errors and some others. Each has its own code and message, the field it is about (null when the problem concerns the whole request) and that field's location: body, query, route or header. |
The error body shares success and data with a success, so a client can read both with one type.
Pagination
Eight lists return every row at once and carry no pagination: List regions, List OS images, List applications, Get PSU health, List console sessions, List virtual media, Get security scan and List scan plugins. Every other list is paged, and all of them take the same parameters:
| Parameter | Default | Notes |
|---|---|---|
page | 1 | 1-based. A page past the last answers an empty data. |
pageSize | 25 | 1 to 1000. A value outside that range is a 400. |
sortColumn | none | The field to sort by. |
ascending | true | false reverses the sort. |
search | none | Free text; leading and trailing spaces are dropped. Which fields it searches depends on the endpoint. |
filter[<field>] | none | One parameter per filtered field. |
The answer's pagination repeats page and pageSize and adds totalRecords and totalPages. To read a whole list, request pages until page reaches totalPages; an empty list answers totalPages 0, so check for that first. Stopping at the first empty page works as well. Every paged list ends its sort on each row's id, so rows that tie on sortColumn keep their order from one page to the next.
curl --request GET \
--url 'https://api.serverside.com/v1/baremetal?page=2&pageSize=100&sortColumn=name' \
--header 'Authorization: Bearer <api-key>'A filter on a text field matches any value that contains it, ignoring case. A filter on a field with allowed values takes the value as the API writes it, filter[status]=CANCELLATION_REQUESTED, and one on a true-or-false field takes true or false. A value the field cannot hold, a name the list cannot filter on, and any filter at all on a list that takes none are refused with 400 VALIDATION_ERROR, on an INVALID_VALUE detail whose field is the key as you sent it (filter[status]). A blank value is no filter.
sortColumn and the names inside filter[...] are not part of the stable contract. A sortColumn the endpoint does not know is ignored, and the list comes back in its default order.
Errors
Statuses
| Status | Meaning | What to do |
|---|---|---|
400 | The request could not be read: malformed JSON, a wrong type, a property the endpoint does not declare, a value outside the declared range, an unknown allowed value, a timestamp without an offset. | Fix the request; the same request fails again. |
401 | The key is missing, wrong, disabled or expired, or the endpoint takes a console session (Authentication). | Send a valid key. |
403 | The key is valid but lacks the permission the endpoint checks. | Use a key that holds it. |
404 | Nothing with that id exists in your organization, or no endpoint has that path. A resource of another organization gets the same answer. | Check the id and the path. |
405, 406, 408, 413, 415 | The method (the Allow header lists the ones the path takes), Accept header, body timing, body size or content type is wrong. | Fix the request. |
409 | Something is in the way: a duplicate, a resource in use, an operation already running. | Retry after Retry-After when the response has one; otherwise resolve the conflict. |
422 | The request was read, and a rule of the resource or its current state refused it. | Change what you asked for. |
500 | A fault on our side. | Retry later, and send the traceId to support if it persists. |
502 | A system behind the API answered with an error. | Retry later. |
503 | A system behind the API cannot be reached, or a feature is paused. Always has Retry-After. | Retry after Retry-After. |
504 | A system behind the API did not answer in time. | Retry later. |
Retry-After is a number of seconds. Every 503 has it, and so does each 409 that clears by itself, such as a management controller that is restarting. Retrying a 500, 502 or 504 is safe for reads. Of the calls that change something, only the five listed under Idempotency take a key that makes a retry safe; before retrying any other, read what exists, since the first request may have gone through.
Validation
Two codes carry a list of details:
VALIDATION_ERROR,400: the request could not be read. A detail'scodeisINVALID_BODYwhen the JSON body itself cannot be read: bad syntax, a wrong type, a missing required member, a member the endpoint does not declare, a value outside a field's allowed values. It isINVALID_VALUEfor a query, route or header value, and for a body field that was read but breaks a declared limit such as a length or a range.VALUE_OUT_OF_RANGEmarks a number the operation checks in its own code.VALIDATION_FAILED,422: the request was read, and one or more rules refused it. Each detail'scodenames the rule, such asBILLING_CYCLE_UNAVAILABLEon an order, and itslocationisnull.
A body field is named by its JSON path, with indexes for arrays (items[0].plan, billingAddress.countryCode). A query field is named as you sent it, a route field by its parameter name and a header by its name. With one detail, message reads field: problem; with several, The request has N problems: followed by each of them.
An order on a billing cycle its plan is not sold on:
{
"success": false,
"data": null,
"code": "VALIDATION_FAILED",
"message": "cycle: This billing cycle is not offered.",
"traceId": "SRV-1A2B-3C4D-5E6F-7A8B",
"details": [
{
"code": "BILLING_CYCLE_UNAVAILABLE",
"message": "This billing cycle is not offered.",
"field": "cycle",
"location": null
}
]
}
An id in the body that matches nothing is treated by what it names. A plan, image, application, segment or interface is a catalog entry, and an unknown one is a 422 with a detail on that field, such as OFFERING_NOT_FOUND on items[0].plan. An id of something your organization owns (a server, a network, an SSH key) is a 404 ENTITY_NOT_FOUND with no field, so another organization's id looks the same as one that does not exist. A datacenter is the exception: an unknown code is a DATACENTER_NOT_FOUND detail, and an unknown UUID is 422 OFFERING_NOT_AVAILABLE.
Codes
Codes that any endpoint can answer:
| Code | Status | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | The request could not be read; see details. |
BAD_REQUEST | 400 | The HTTP request itself could not be read. |
UNAUTHENTICATED | 401 | See Authentication errors. |
NO_PERMISSION | 403 | The key lacks the permission the endpoint names. |
ENTITY_NOT_FOUND | 404 | No resource with that id in your organization. |
ROUTE_NOT_FOUND | 404 | No endpoint has that path. |
METHOD_NOT_ALLOWED | 405 | The path exists, the method does not; Allow lists the methods it takes. |
ENTITY_ALREADY_EXISTS | 409 | A resource with the same identity exists. |
ENTITY_IN_USE | 409 | The resource is in use, so it cannot be deleted or moved. |
ENTITY_STATE_INVALID | 422 | The resource's state does not allow the operation, where no more specific code exists. |
VALIDATION_FAILED | 422 | A rule refused the request; see details. |
INTERNAL_ERROR | 500 | A fault on our side. |
UPSTREAM_ERROR | 502 | A system behind the API answered with an error. |
UPSTREAM_UNAVAILABLE | 503 | A system behind the API cannot be reached. Retry-After: 30. |
UPSTREAM_TIMEOUT | 504 | A system behind the API timed out. |
Codes for bare metal servers:
| Code | Status | Meaning |
|---|---|---|
SERVICE_SUSPENDED | 422 | The service is suspended; nothing can change until it is reinstated. |
SERVICE_LOCKED | 422 | The service is locked. |
SERVICE_DELETING | 422 | The service is being deleted, and refuses everything a suspended one does. |
OPERATION_IN_PROGRESS | 409 | Another operation is running on the server. |
BMC_RESET_IN_PROGRESS | 409 | The management controller is restarting. Retry-After: 30. |
BMC_UNREACHABLE | 503 | The management controller is not responding. Retry-After: 30. |
BMC_REQUEST_FAILED | 502 | The management controller returned an error. |
KVM_SESSION_REQUEST_IN_PROGRESS | 409 | A console session request for the server is already running. Retry-After: 10. |
OPERATION_NOT_SUPPORTED_PLATFORM | 422 | The server's hardware does not support the operation. |
PRIMARY_IPV4_REQUIRED | 422 | A deployment needs a primary IPv4 address on the server. |
ISO_LIMIT_REACHED | 422 | The organization holds as many ISOs as it may. |
ISO_UPLOAD_INCOMPLETE | 422 | The ISO's upload has not finished or could not be verified. |
VIRTUAL_MEDIA_EJECT_FAILED | 502 | The management controller could not eject the media. |
VIRTUAL_MEDIA_ATTACH_TIMEOUT | 504 | Attaching virtual media timed out. |
NETWORK_METRICS_UNAVAILABLE | 503 | Traffic metrics cannot be read for now. Retry-After: 30. |
Codes for orders and billing:
| Code | Status | Meaning |
|---|---|---|
OFFERING_NOT_AVAILABLE | 422 | The plan cannot be ordered as asked: out of stock in that datacenter, no address space there for it, or an unknown datacenter UUID. |
USAGE_BILLING_DISABLED | 422 | Hourly billing is not enabled for the organization (Quickstart). |
BILLING_ADDRESS_REQUIRED | 422 | The organization has no billing address. |
PAYMENT_FAILED | 422 | The payment could not be taken: the card was declined, or the organization has no account credit; data holds the payment result. |
INVOICE_CANNOT_ACCEPT_PAYMENT | 422 | The invoice is already paid, cancelled or refunded, has nothing due, or a payment is awaiting confirmation. |
SUBSCRIPTION_NOT_ACTIVE | 422 | The subscription has already been cancelled. |
SUBSCRIPTION_SERVER_TRANSFERRED | 422 | A server the subscription bills now belongs to another organization; nothing changed. |
TERMINATION_BLOCKED | 422 | An item on the subscription cannot be cancelled now; nothing changed. |
BUNDLED_ITEM_CANCELLATION_BLOCKED | 422 | The IP block came with a server; deleting the server releases it. |
SUBSCRIPTION_CANCELLATION_INCOMPLETE | 422 | Some items were cancelled before one failed. Sending the request again is safe. |
IDEMPOTENCY_KEY_REUSED | 409 | The Idempotency-Key was already used for a different request. |
ORDER_CREATION_IN_PROGRESS | 409 | A request with the same Idempotency-Key is still creating the order. Retry-After: 5. |
ORDER_PROVISIONING_IN_PROGRESS | 409 | A create under the same Idempotency-Key is still running. Retry-After: 5. |
PROVISIONING_FAILED | 409 | Nothing in an hourly order, or in an hourly server or IP block request, could be provisioned. |
VAT_SERVICE_UNAVAILABLE | 503 | The EU VAT number check could not be completed. Retry-After: 30. |
STRIPE_PROVIDER_ERROR | 502 | The payment provider failed. |
Networking codes are listed in Managing IP addresses and Setting up virtual networks, next to the calls that answer them. VALIDATION_FAILED detail codes are named in the guide for the operation that sends them.
Idempotency
Five operations accept an Idempotency-Key header of up to 200 characters: creating an order, creating a bare metal service, creating an IP block, and attaching or detaching a server on a virtual network. Send a fresh value for each new action and the same value when you retry that action.
The three create calls share one set of keys per organization, and a key is never forgotten. A retry with the same key and the same purchase (billing cycle, promo code and items) places nothing new and answers 200 in place of 201, with what the first request created; Create order also sets data.isReplayed to true. Naming the plan by its slug in one request and by its id in the other still counts as the same purchase, and so does a datacenter's code against its id. The same key with a different purchase is refused with 409 IDEMPOTENCY_KEY_REUSED. While the first request is still running, a retry gets 409 ORDER_CREATION_IN_PROGRESS or ORDER_PROVISIONING_IN_PROGRESS with Retry-After: 5; send it again after that many seconds.
On the virtual network calls a key belongs to the server, and it is remembered until 90 days after the change has been applied. A retry answers 202 again without queuing a second change, and the same key with another network or segment gets 409 IDEMPOTENCY_KEY_REUSED. A second change while the first is still being applied gets 409 NETWORK_CHANGE_IN_PROGRESS with Retry-After: 10.
Rate limits
Each API key has its own limits:
| Requests | Limit |
|---|---|
Reads (GET) | 1,000 per minute |
Writes (POST, PUT, PATCH, DELETE) | 100 per minute |
The limits are enforced in front of the API, and the API itself does not send 429.
Polling a deployment every 20 seconds, as the guides do, uses 3 reads a minute.