Rapids Volumes API
Add a volume to a Rapids container, make it larger, back it up and restore a backup, from a script, a CI job or an AI agent. The API does what the console does: the same rules, the same limits and the same prices.
Before you start
- The token needs an ability.
serverless:readlists and reads volumes, backups and restores.serverless:writeadds a volume, makes it larger, moves it, takes a backup and restores one.serverless:deletedeletes a volume or a backup. A token with all abilities, such as the onedanube logincreates, can do all of it. See Authentication. - Everything belongs to a container. Every address starts with
/api/v1/serverless/{container}, where{container}is the container's id. The container has to be in the project the token is bound to; with an account-wide token, send the project's id in theX-Team-Idheader. - Writes are asynchronous. A request that is taken answers
202 Acceptedwith the thing as it is now, andmeta.poll_urlsays where to look again. Nothing is finished when the answer comes. - Volumes have to be offered to your account. If they are not, reading and deleting still work, and everything that would buy storage is refused with
serverless.volume_not_available.
Endpoints
The addresses below are relative to https://danubedata.ro/api/v1.
| Method | Endpoint | Description | Ability |
|---|---|---|---|
| GET | /serverless/{container}/volumes | List the container's volumes, and what is on offer | serverless:read |
| POST | /serverless/{container}/volumes | Add a volume | serverless:write |
| GET | /serverless/{container}/volumes/{volume} | Show a volume | serverless:read |
| PUT or PATCH | /serverless/{container}/volumes/{volume} | Make the volume larger, or change its mount path | serverless:write |
| DELETE | /serverless/{container}/volumes/{volume} | Delete the volume | serverless:delete |
| GET | /serverless/{container}/volume-backups | List the backups, and what is on offer | serverless:read |
| POST | /serverless/{container}/volume-backups | Take a backup | serverless:write |
| GET | /serverless/{container}/volume-backups/{backup} | Show a backup | serverless:read |
| DELETE | /serverless/{container}/volume-backups/{backup} | Delete a backup | serverless:delete |
| POST | /serverless/{container}/volume-backups/{backup}/restore | Restore a backup | serverless:write |
| GET | /serverless/{container}/volume-restores | List the last twenty restores | serverless:read |
| GET | /serverless/{container}/volume-restores/{restore} | Show a restore | serverless:read |
Every volume, backup and restore is addressed by its id. A container has one volume for now, but that is a limit and not the shape of the address: a request about a volume reaches the volume it was made for, also after a volume has been deleted and another added. A backup or a restore is made of, or into, the volume the container has when the request runs.
The full schema of every request and response is in the OpenAPI document.
The answer
Every answer is the same envelope:
{
"success": true,
"data": { "...": "the thing, or a list of them" },
"error": null,
"meta": {}
}
A refusal has "success": false, "data": null and an error:
{
"success": false,
"data": null,
"error": {
"code": "serverless.volume_container_busy",
"message": "A change to this container is in progress. Wait for it to finish, or cancel it, and try again.",
"retryable": true,
"field": null
},
"meta": {}
}
error.codeis stable: branch on it. The message is for people and may be reworded. Every code is in Failure codes, with its HTTP status.error.retryablesays whether the same request can succeed later with nothing changed on your side.error.fieldnames the field of your request that a refusal is about, such assize_gbormount_path, or isnull.- A body that is not valid (a field missing, a size that is not a number or is outside 1 to 50 GB, a mount path the container cannot use) is answered
422the way the rest of the API answers validation:{"message": "...", "errors": {"size_gb": ["..."]}}. - A token without the ability, a container that is not in the project and a request with no token answer
401or403with{"message": "...", "error": "..."}. A container id that does not exist answers404with{"message": "Not Found", "error": "Not Found"}.
Volumes
What is on offer
curl 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volumes' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
{
"success": true,
"data": [
{
"id": "0199d1f0-4b7e-7c3a-9a40-3f6a1c5e8b21",
"size_gb": 10,
"mount_path": "/data",
"status": "active",
"billed": true,
"monthly_cost_cents": 120,
"failure": null,
"created_at": "2026-10-08T09:14:22+00:00",
"updated_at": "2026-10-08T09:15:41+00:00"
}
],
"error": null,
"meta": {
"total": 1,
"offer": {
"available": true,
"min_size_gb": 1,
"max_size_gb": 50,
"cents_per_gb_month": 12,
"team_limit_gb": 100,
"team_remaining_gb": 90
}
}
}
The volume the container mounts comes first, then any that is being deleted or never came up. meta.offer says whether volumes are offered to the account, the least and the most a volume may be, the price in cents for a GB a month (null when it could not be read), and how much more the project may still hold.
monthly_cost_cents is the price of a full month at the size the volume has, or null when the price cannot be found at that moment.
Add a volume
curl -X POST 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volumes' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Idempotency-Key: 5d1c0e3a-7a49-4d6c-8e0e-4b8f6a3b9c10' \
-d '{"size_gb": 10, "mount_path": "/data"}'
| Field | Required | Values |
|---|---|---|
size_gb | Yes | A whole number, 1 to 50, and no more than the project may still hold |
mount_path | Yes | An absolute path such as /data. See Mount path |
The answer is 202 with the volume ("status": "pending") and meta.poll_url. The container is rolled out to mount it. Follow the volume at meta.poll_url until status is active. A container has one volume: a second POST answers 409 serverless.volume_already_exists.
Follow a volume
GET /serverless/{container}/volumes/{volume} answers the volume as it is. A volume that is being deleted stays at its address with "status": "deleting" until the platform has removed it; then the address answers 404 serverless.volume_not_found.
status is pending (being provisioned), active, resizing, restoring or deleting. failed is reserved for a volume that could not be created at all; the platform does not set it today. A volume that is not provisioned after 10 minutes stays pending with failure.operation provision: delete it and add it again. failure is null, or says what went wrong last: operation is one of provision, resize, delete and restore, and message says why. A volume whose last operation failed is usually still active with the data it had.
Make the volume larger, or move it
curl -X PATCH 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volumes/VOLUME_ID' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"size_gb": 20}'
Send size_gb or mount_path, one of them per request (PUT and PATCH do the same). A size has to be larger than the current one. The answer is 202 with the volume ("status": "resizing" for a larger size), or 200 with "meta": {"changed": false} when the volume has the size or the path you asked for already, so asking twice changes nothing.
A larger size is billed from the moment you ask. The file system of a running instance grows only when the volume is next mounted: stop the container and start it again to use the space at once. A new mount path rolls the container out; the files do not move.
Delete a volume
DELETE /serverless/{container}/volumes/{volume} answers 202 with the volume as deleting. Deleting a volume that is being deleted answers 202 again. The files are deleted once nothing mounts the volume. Backups are kept.
Backups
What is on offer
GET /serverless/{container}/volume-backups lists the backups, newest first, with:
"meta": {
"total": 2,
"offer": {
"available": true,
"max_per_container": 10,
"cents_per_gb_month": 7.3,
"monthly_cost_cents": 11,
"stored_bytes": 1572864000
}
}
stored_bytes is what the container's backups take in the backup store now, which is what you are billed for.
Take a backup
curl -X POST 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volume-backups' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"name": "before the migration"}'
name is optional, up to 100 characters. Left out, the backup is named after the time it was taken. The answer is 202 with the backup:
{
"success": true,
"data": {
"id": "0199d1f4-0b52-7e11-8c0d-6a2d9e47b3f0",
"name": "before the migration",
"status": "pending",
"terminal": false,
"progress": null,
"size_bytes": null,
"volume_id": "0199d1f0-4b7e-7c3a-9a40-3f6a1c5e8b21",
"volume_size_gb": 10,
"mount_path": "/data",
"failure": null,
"created_at": "2026-10-08T10:02:09+00:00",
"ready_at": null,
"deleting_since": null
},
"error": null,
"meta": { "poll_url": "/api/v1/serverless/CONTAINER_ID/volume-backups/0199d1f4-0b52-7e11-8c0d-6a2d9e47b3f0", "poll_after_ms": 3000 }
}
Poll the backup until terminal is true: ready (it can be restored) or failed. status is pending, creating (while it uploads, with progress from 0 to 100), ready, failed or deleting. A backup that is deleting is not over until it is gone; terminal stays false and the address answers 404 afterwards.
size_bytes is what the backup takes in the backup store: the space it is billed for, null until it is known. It can be larger than the files on the volume.
At most 10 backups are kept per container, one is made at a time, two are at least 10 minutes apart, and 24 can be requested in any 24 hours, deleted ones included. See the limits.
Delete a backup
DELETE /serverless/{container}/volume-backups/{backup} answers 202 with the backup as deleting. A backup that is still being made is stopped. It takes the backup store at least five minutes, and half a minute more for every GB the backup held (an hour at most), to remove the data; until then the backup stays in the list and no other backup can be taken or restored.
Restores
Restore a backup
curl -X POST 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volume-backups/BACKUP_ID/restore' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Idempotency-Key: 9a7e3b52-1c0d-4f6a-b8d4-2e5c7f1a0b63'
A restore replaces the files on the volume with the backup's. What was written after the backup was taken, including what is written while the restore runs, is lost. A container that has no volume gets one made from the backup; mount_path is optional then (the path the backup was taken at is used); for a container that has a volume it is not used, but it is still checked.
The answer is 202 with the restore and meta.poll_url:
{
"success": true,
"data": {
"id": "0199d1f9-6c21-7a40-8f13-0d5b7a9e2c84",
"backup_id": "0199d1f4-0b52-7e11-8c0d-6a2d9e47b3f0",
"backup_name": "before the migration",
"status": "pending",
"terminal": false,
"landed": false,
"creates_volume": false,
"target_size_gb": 10,
"failure": null,
"started_at": "2026-10-08T10:20:31+00:00",
"finished_at": null
},
"error": null,
"meta": { "poll_url": "/api/v1/serverless/CONTAINER_ID/volume-restores/0199d1f9-6c21-7a40-8f13-0d5b7a9e2c84", "poll_after_ms": 3000 }
}
Poll the restore at meta.poll_url until terminal is true: completed (the container serves the restored data) or failed (failure.message says why, and the volume is as it was). status is pending, downloading (the backup is being copied back), growing, ready, switched (the container is being rolled out onto the restored data), completed or failed. landed is true once the container serves the restored data; what is left is removing the copy it replaced.
The copy takes about 20 to 25 seconds for every GB of data in the backup, plus up to two minutes. The restore is finished a few minutes after that, when the container serves the restored data (landed, then completed). While it runs, the volume cannot be made larger, moved, deleted or backed up. GET /serverless/{container}/volume-restores lists the last twenty; meta.total is how many the container has had.
Retry safely
A restore replaces what has been written since the backup, so a restore must not run twice by accident. If the connection drops before you read the answer, you cannot tell whether it started.
- Send an
Idempotency-Keywith every write, and withPOST …/restoreabove all. Retry with the same key and the same body: you get the first answer back, markedIdempotent-Replay: true, and nothing is started twice. - Without a key, a restore that is under way is refused with
serverless.volume_restore_in_progress. That code is not retryable, andmetanames the restore that is running (restore_id,poll_url): follow that one. Asking again once it has finished would restore a second time. - A restore that failed holds its place for a few minutes while the platform cleans up after it: a restore request is then answered
serverless.volume_restore_cleaning_up, which is retryable and also names the restore. Adding a volume in that state is answeredserverless.volume_restore_in_progress, naming the failed restore. - A key is yours alone, up to 255 characters of text, and is remembered for the address it was used on. The same key with another body answers
409idempotency.key_reused. A request that was refused does not use up its key.
See Support Tickets API for the full rules of keys.
Limits on requests
The six writes (add, change and delete a volume, take and delete a backup, restore) share one limit: 20 a minute for one person and one container. Beyond it the answer is 429 with {"message": "...", "retry_after": 12} and a Retry-After header with the seconds to wait. A request that is answered again from its Idempotency-Key counts too. Reads are not counted here; the API's rate limits apply to all of them.
Errors
| Status | Meaning |
|---|---|
202 | Taken. The thing is in the body; meta.poll_url says where to look again |
200 | Read, or a change that was a repeat (meta.changed is false) |
401, 403 | No token, a token without the ability, or a container that is not in the project. A volume that is not offered to the account answers 403 with an error.code |
404 | The container has no such volume, backup or restore: serverless.volume_not_found, serverless.volume_backup_not_found, serverless.volume_restore_not_found |
409 | The state of the container, the volume or the backups does not allow it now. retryable says whether it will pass |
422 | The request itself is wrong. A size outside 1 to 50 GB, a path the container cannot mount and a body that is not valid are answered by the validation form (errors.size_gb, errors.mount_path). A smaller size than the volume has is answered serverless.volume_cannot_shrink |
429 | More than 20 changes a minute |
503 | The platform cannot do it now (no room, or it could not work out where to place the volume). Retryable |
Every error.code is listed in Failure codes.