REST API
SdV Cloud exposes a JSON:API-compliant REST API for managing organizations, projects, service containers, and deployments, scoped to a single organization per token.
Base URL
Section titled “Base URL”All endpoints are relative to:
http://localhost/api/v1Authentication & scoping
Section titled “Authentication & scoping”Requests are authenticated with a personal access token, generated from Settings → API Tokens — the same token type used by the MCP server.
Authorization: Bearer <your-api-token>Every token is created with:
- One organization — encoded as an internal
org:{organization_id}ability at creation time. All requests made with the token operate within that organization only, and this cannot be changed later. To act within a different organization, revoke the token and generate a new one scoped to it. - One or more abilities, which gate which endpoints the token can call. These are the same abilities used by the MCP server:
| Ability | Grants |
|---|---|
projects.read |
GET /projects, GET /projects/{project} |
projects.write |
POST /projects, PATCH /projects/{project}, DELETE /projects/{project} |
service-containers.read |
GET /service-containers, GET /service-containers/{serviceContainer} |
service-containers.write |
POST /service-containers, PATCH /service-containers/{serviceContainer}, DELETE /service-containers/{serviceContainer} |
deployments.read |
GET /deployments, GET /deployments/{deployment} |
deployments.write |
POST /service-containers/{serviceContainer}/deployments, POST /deployments/{deployment}/cancel |
A single token works for both the REST API and the MCP server. Calling an endpoint without the required ability returns a 403 Forbidden (see Errors) rather than a generic authentication failure. Beyond token abilities, some actions are further gated by your role in the organization or project (for example, only organization Owners can create projects).
GET /organization requires no specific ability — access is governed entirely by the token’s organization scope.
Making requests
Section titled “Making requests”Every request must send:
Authorization: Bearer <your-api-token>Accept: application/vnd.api+jsonPOST and PATCH requests that include a body must also send:
Content-Type: application/jsonAll responses — including errors — are returned as application/vnd.api+json.
Response format
Section titled “Response format”Responses follow the JSON:API document structure: every resource is wrapped in a top-level data object (or array, for collections) with id, type, and attributes. Attribute keys are camelCase.
{ "data": { "id": "01991a00-0000-7000-8000-000000000001", "type": "projects", "attributes": { "name": "astro-blog", "description": "Marketing blog" } }}Relationships and includes
Section titled “Relationships and includes”Resources that belong to another resource (a service container’s project, a deployment’s service container and stages) expose that relationship as a reference — an id and type, without the related resource’s attributes:
"relationships": { "project": { "data": { "id": "01991a00-0000-7000-8000-000000000001", "type": "projects" } }}Pass ?include=<relationship> to also embed the full related resource(s) in a top-level included array. For example, GET /deployments/{deployment}?include=stages embeds each deployment stage.
Pagination
Section titled “Pagination”List endpoints (GET /projects, GET /service-containers, GET /deployments) are paginated, 15 items per page by default:
GET /projects?page=2{ "data": [ ... ], "links": { "first": "http://localhost/api/v1/projects?page=1", "last": "http://localhost/api/v1/projects?page=3", "prev": "http://localhost/api/v1/projects?page=1", "next": "http://localhost/api/v1/projects?page=3" }, "meta": { "current_page": 2, "from": 16, "last_page": 3, "per_page": 15, "to": 30, "total": 42 }}Filtering and sorting
Section titled “Filtering and sorting”List endpoints accept filter[...] query parameters and a sort parameter. Prefix a sort field with - for descending order (e.g. sort=-createdAt). Supported fields are documented per endpoint below.
Errors
Section titled “Errors”Errors follow the JSON:API error object format — a top-level errors array, even when there’s a single error:
{ "errors": [ { "status": "404", "title": "Not Found", "detail": "The requested resource could not be found." } ]}| Status | When | Notes |
|---|---|---|
401 Unauthenticated |
Missing or invalid bearer token | |
403 Forbidden |
Token lacks the required ability, or your role doesn’t permit the action | detail: "Invalid ability provided." for a missing ability |
404 Not Found |
The resource doesn’t exist, or belongs to a different organization | A project (or any resource) outside your token’s organization always 404s, never 403s — organizations can’t detect each other’s resources |
409 Conflict |
The action can’t be performed in the resource’s current state | e.g. deleting a service container with a deployment in progress, or starting a deployment while one is already running |
422 Unprocessable Entity |
Request body failed validation | One error object per failing field/rule, each with a source.pointer (e.g. /data/attributes/name) |
429 Too Many Requests |
Rate limit exceeded | See Rate limits |
Validation errors report every failing field:
{ "errors": [ { "status": "422", "title": "Unprocessable Entity", "detail": "The name field is required.", "source": { "pointer": "/data/attributes/name" } } ]}Rate limits
Section titled “Rate limits”Requests are limited to 60 per minute, keyed per authenticated user (shared across all of that user’s tokens). Exceeding the limit returns 429 Too Many Requests.
Endpoints
Section titled “Endpoints”Organization
Section titled “Organization”Get the current organization
Section titled “Get the current organization”GET /organizationReturns the organization your token is scoped to.
curl http://localhost/api/v1/organization \ -H "Authorization: Bearer <your-api-token>" \ -H "Accept: application/vnd.api+json"const response = await fetch('http://localhost/api/v1/organization', { headers: { Authorization: 'Bearer <your-api-token>', Accept: 'application/vnd.api+json', },});const { data } = await response.json();$response = $client->get('http://localhost/api/v1/organization', [ 'headers' => [ 'Authorization' => 'Bearer <your-api-token>', 'Accept' => 'application/vnd.api+json', ],]);
$data = json_decode($response->getBody(), true)['data'];{ "data": { "id": "01991a00-0000-7000-8000-0000000000f0", "type": "organizations", "attributes": { "name": "Acme Inc.", "mfaRequired": false } }}Projects
Section titled “Projects”List projects
Section titled “List projects”GET /projectsRequires projects.read. Returns the projects you’re a member of (organization Owners see every project).
| Query parameter | Description |
|---|---|
filter[name] |
Filter to projects whose name contains this value |
sort |
name or createdAt, prefix with - for descending |
page |
Page number |
curl "http://localhost/api/v1/projects?filter[name]=blog&sort=-createdAt" \ -H "Authorization: Bearer <your-api-token>" \ -H "Accept: application/vnd.api+json"const response = await fetch( 'http://localhost/api/v1/projects?filter[name]=blog&sort=-createdAt', { headers: { Authorization: 'Bearer <your-api-token>', Accept: 'application/vnd.api+json', }, },);const { data } = await response.json();$response = $client->get('http://localhost/api/v1/projects', [ 'headers' => [ 'Authorization' => 'Bearer <your-api-token>', 'Accept' => 'application/vnd.api+json', ], 'query' => ['filter' => ['name' => 'blog'], 'sort' => '-createdAt'],]);
$data = json_decode($response->getBody(), true)['data'];Returns an array of project resources (see the response shape below), paginated.
Create a project
Section titled “Create a project”POST /projectsRequires projects.write and the organization Owner role.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Project name (max 255 characters), unique within the organization |
description |
string | No | Project description (max 1000 characters) |
curl -X POST http://localhost/api/v1/projects \ -H "Authorization: Bearer <your-api-token>" \ -H "Accept: application/vnd.api+json" \ -H "Content-Type: application/json" \ -d '{"name": "astro-blog", "description": "Marketing blog"}'const response = await fetch('http://localhost/api/v1/projects', { method: 'POST', headers: { Authorization: 'Bearer <your-api-token>', Accept: 'application/vnd.api+json', 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'astro-blog', description: 'Marketing blog' }),});const { data } = await response.json();$response = $client->post('http://localhost/api/v1/projects', [ 'headers' => [ 'Authorization' => 'Bearer <your-api-token>', 'Accept' => 'application/vnd.api+json', ], 'json' => ['name' => 'astro-blog', 'description' => 'Marketing blog'],]);
$data = json_decode($response->getBody(), true)['data'];Returns 201 Created with a Location header pointing at the new project.
{ "data": { "id": "01991a00-0000-7000-8000-000000000001", "type": "projects", "attributes": { "name": "astro-blog", "description": "Marketing blog" } }}Get a project
Section titled “Get a project”GET /projects/{project}Requires projects.read. Same response shape as Create a project.
Update a project
Section titled “Update a project”PATCH /projects/{project}Requires projects.write.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Project name (max 255 characters), unique within the organization |
description |
string | No | Project description (max 1000 characters) |
Returns the updated project, same shape as Create a project.
Delete a project
Section titled “Delete a project”DELETE /projects/{project}Requires projects.write. Returns 204 No Content.
Service containers
Section titled “Service containers”A service container’s gitProvider and gitRepositoryName are fixed at creation and can’t be changed afterwards.
List service containers
Section titled “List service containers”GET /service-containersRequires service-containers.read.
| Query parameter | Description |
|---|---|
filter[projectId] |
Only service containers belonging to this project |
filter[name] |
Filter to service containers whose name contains this value |
sort |
name or createdAt, prefix with - for descending |
page |
Page number |
Returns an array of service container resources, paginated.
Create a service container
Section titled “Create a service container”POST /service-containersRequires service-containers.write and the organization Owner or project Developer role. Only gitProvider: gitlab currently provisions a deploy token automatically.
| Field | Type | Required | Description |
|---|---|---|---|
projectId |
string | Yes | UUID of the project to create the service container in |
name |
string | Yes | Service container name (max 255 characters), unique within the project |
containerType |
string | Yes | One of http, worker, scheduled |
gitProvider |
string | Yes | One of gitlab, github |
gitRepositoryId |
string | Yes | Provider-specific repository id |
gitRepositoryName |
string | Yes | Repository path, e.g. my-group/my-repo |
curl -X POST http://localhost/api/v1/service-containers \ -H "Authorization: Bearer <your-api-token>" \ -H "Accept: application/vnd.api+json" \ -H "Content-Type: application/json" \ -d '{ "projectId": "01991a00-0000-7000-8000-000000000001", "name": "api", "containerType": "http", "gitProvider": "gitlab", "gitRepositoryId": "123456", "gitRepositoryName": "my-group/api" }'const response = await fetch('http://localhost/api/v1/service-containers', { method: 'POST', headers: { Authorization: 'Bearer <your-api-token>', Accept: 'application/vnd.api+json', 'Content-Type': 'application/json', }, body: JSON.stringify({ projectId: '01991a00-0000-7000-8000-000000000001', name: 'api', containerType: 'http', gitProvider: 'gitlab', gitRepositoryId: '123456', gitRepositoryName: 'my-group/api', }),});const { data } = await response.json();$response = $client->post('http://localhost/api/v1/service-containers', [ 'headers' => [ 'Authorization' => 'Bearer <your-api-token>', 'Accept' => 'application/vnd.api+json', ], 'json' => [ 'projectId' => '01991a00-0000-7000-8000-000000000001', 'name' => 'api', 'containerType' => 'http', 'gitProvider' => 'gitlab', 'gitRepositoryId' => '123456', 'gitRepositoryName' => 'my-group/api', ],]);
$data = json_decode($response->getBody(), true)['data'];Returns 201 Created with a Location header. The new service container isn’t deployable yet — configure branchToDeploy and port with Update a service container, then trigger a deployment.
{ "data": { "id": "01991a00-0000-7000-8000-000000000002", "type": "service-containers", "attributes": { "name": "api", "containerType": "http", "gitProvider": "gitlab", "gitRepositoryName": "my-group/api", "branchToDeploy": null, "buildMode": "automatic", "dockerfilePath": null, "port": null, "memoryLimit": 512, "cpuLimit": 0.5, "environmentVariables": [], "autoscalingEnabled": false, "minReplicas": null, "maxReplicas": null, "cpuThreshold": null, "hibernationEnabled": false, "hibernationSchedules": null, "ipAllowlistEnabled": false, "ipAllowlist": null }, "relationships": { "project": { "data": { "id": "01991a00-0000-7000-8000-000000000001", "type": "projects" } } } }}Get a service container
Section titled “Get a service container”GET /service-containers/{serviceContainer}Requires service-containers.read. Pass ?include=project to embed the parent project. Unlike the MCP server, the REST API returns environment variable values, not just keys — treat responses accordingly.
Update a service container
Section titled “Update a service container”PATCH /service-containers/{serviceContainer}Requires service-containers.write. All fields are optional and only replace what’s provided; array fields (environmentVariables, hibernationSchedules, ipAllowlist) are replaced wholesale when passed.
| Field | Type | Description |
|---|---|---|
name |
string | Service container name (max 255 characters), unique within the project |
branchToDeploy |
string | Git branch to deploy |
buildMode |
string | One of automatic, dockerfile |
dockerfilePath |
string | Path to the Dockerfile, relative to the repository root (required with buildMode: dockerfile) |
port |
integer | Exposed port (1-65535) |
memoryLimit |
integer | Memory limit in MB (250-8000) |
cpuLimit |
number | CPU limit in cores (0.25-4) |
environmentVariables |
array of {key, value} |
Replaces all environment variables |
autoscalingEnabled |
boolean | Enable CPU-based autoscaling |
minReplicas |
integer | Minimum replica count (1-10) |
maxReplicas |
integer | Maximum replica count (1-10) |
cpuThreshold |
integer | CPU percentage that triggers scaling (10-90) |
hibernationEnabled |
boolean | Enable scheduled hibernation |
hibernationSchedules |
array of {days, startTime, endTime} |
Replaces all hibernation schedules. days is an array of mon-sun; startTime/endTime are HH:MM (24-hour) |
ipAllowlistEnabled |
boolean | Restrict access to an IP allowlist |
ipAllowlist |
array of {ip, description} |
Replaces the IP allowlist |
Returns the updated service container, same shape as Create a service container.
Delete a service container
Section titled “Delete a service container”DELETE /service-containers/{serviceContainer}Requires service-containers.write. Returns 204 No Content, or 409 Conflict if a deployment is in progress.
Deployments
Section titled “Deployments”Trigger a deployment
Section titled “Trigger a deployment”POST /service-containers/{serviceContainer}/deploymentsRequires deployments.write. Starts a new deployment asynchronously and returns immediately with the created deployment in pending status — poll Get a deployment to follow its progress through the build/deploy stages.
curl -X POST http://localhost/api/v1/service-containers/01991a00-0000-7000-8000-000000000002/deployments \ -H "Authorization: Bearer <your-api-token>" \ -H "Accept: application/vnd.api+json"const response = await fetch( 'http://localhost/api/v1/service-containers/01991a00-0000-7000-8000-000000000002/deployments', { method: 'POST', headers: { Authorization: 'Bearer <your-api-token>', Accept: 'application/vnd.api+json', }, },);const { data } = await response.json();$response = $client->post( 'http://localhost/api/v1/service-containers/01991a00-0000-7000-8000-000000000002/deployments', [ 'headers' => [ 'Authorization' => 'Bearer <your-api-token>', 'Accept' => 'application/vnd.api+json', ], ],);
$data = json_decode($response->getBody(), true)['data'];Returns 202 Accepted with a Location header pointing at the deployment. Fails with 409 Conflict if the service container isn’t fully configured yet, or already has a deployment in progress.
{ "data": { "id": "01991a00-0000-7000-8000-000000000003", "type": "deployments", "attributes": { "status": "pending", "triggeredVia": "api", "triggeredByUserId": null, "commitSha": null, "commitMessage": null, "branch": "main", "imageTag": null, "errorMessage": null, "startedAt": null, "finishedAt": null, "createdAt": "2026-07-24T09:00:00.000000Z" }, "relationships": { "serviceContainer": { "data": { "id": "01991a00-0000-7000-8000-000000000002", "type": "service-containers" } }, "stages": { "data": [] } } }}List deployments
Section titled “List deployments”GET /deploymentsRequires deployments.read.
| Query parameter | Description |
|---|---|
filter[serviceContainerId] |
Only deployments for this service container |
filter[status] |
One of pending, building, deploying, succeeded, failed, cancelled |
sort |
createdAt, prefix with - for descending |
page |
Page number |
Returns an array of deployment resources (same shape as Trigger a deployment), paginated.
Get a deployment
Section titled “Get a deployment”GET /deployments/{deployment}Requires deployments.read. Pass ?include=stages to embed each build/deploy stage in included:
{ "data": { "id": "01991a00-0000-7000-8000-000000000003", "type": "deployments", "attributes": { "status": "succeeded", "triggeredVia": "api", "...": "..." }, "relationships": { "stages": { "data": [ { "id": "01991a00-0000-7000-8000-000000000004", "type": "deployment-stages" } ] } } }, "included": [ { "id": "01991a00-0000-7000-8000-000000000004", "type": "deployment-stages", "attributes": { "name": "build", "status": "success", "duration": 42 } } ]}Cancel a deployment
Section titled “Cancel a deployment”POST /deployments/{deployment}/cancelRequires deployments.write. Only deployments in pending, building, or deploying status can be cancelled — otherwise returns 409 Conflict. Returns 204 No Content.