{"slug":"api-serverless-volumes","title":"Rapids Volumes API","description":"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...","section":"API Reference","url":"https://docs.danubedata.ro/api-serverless-volumes","markdown_url":"https://docs.danubedata.ro/api-serverless-volumes.md","breadcrumbs":[{"title":"API Reference","slug":null},{"title":"Rapids Volumes","slug":"api-serverless-volumes"}],"headings":[{"level":1,"title":"Rapids Volumes API","id":"rapids-volumes-api"},{"level":2,"title":"Before you start","id":"before-you-start"},{"level":2,"title":"Endpoints","id":"endpoints"},{"level":2,"title":"The answer","id":"the-answer"},{"level":2,"title":"Volumes","id":"volumes"},{"level":3,"title":"What is on offer","id":"what-is-on-offer"},{"level":3,"title":"Add a volume","id":"add-a-volume"},{"level":3,"title":"Follow a volume","id":"follow-a-volume"},{"level":3,"title":"Make the volume larger, or move it","id":"make-the-volume-larger-or-move-it"},{"level":3,"title":"Delete a volume","id":"delete-a-volume"},{"level":2,"title":"Backups","id":"backups"},{"level":3,"title":"What is on offer","id":"what-is-on-offer"},{"level":3,"title":"Take a backup","id":"take-a-backup"},{"level":3,"title":"Delete a backup","id":"delete-a-backup"},{"level":2,"title":"Restores","id":"restores"},{"level":3,"title":"Restore a backup","id":"restore-a-backup"},{"level":2,"title":"Retry safely","id":"retry-safely"},{"level":2,"title":"Limits on requests","id":"limits-on-requests"},{"level":2,"title":"Errors","id":"errors"}],"format":"markdown","word_count":2605,"content":"# Rapids Volumes API\n\nAdd a [volume](https://docs.danubedata.ro/serverless-volumes) 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.\n\n## Before you start\n\n- **The token needs an ability.** `serverless:read` lists and reads volumes, backups and restores. `serverless:write` adds a volume, makes it larger, moves it, takes a backup and restores one. `serverless:delete` deletes a volume or a backup. A token with all abilities, such as the one `danube login` creates, can do all of it. See [Authentication](https://docs.danubedata.ro/api-authentication).\n- **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 the `X-Team-Id` header.\n- **Writes are asynchronous.** A request that is taken answers `202 Accepted` with the thing as it is now, and `meta.poll_url` says where to look again. Nothing is finished when the answer comes.\n- **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`.\n\n## Endpoints\n\nThe addresses below are relative to `https://danubedata.ro/api/v1`.\n\n| Method | Endpoint | Description | Ability |\n|---|---|---|---|\n| GET | `/serverless/{container}/volumes` | List the container's volumes, and what is on offer | `serverless:read` |\n| POST | `/serverless/{container}/volumes` | Add a volume | `serverless:write` |\n| GET | `/serverless/{container}/volumes/{volume}` | Show a volume | `serverless:read` |\n| PUT or PATCH | `/serverless/{container}/volumes/{volume}` | Make the volume larger, or change its mount path | `serverless:write` |\n| DELETE | `/serverless/{container}/volumes/{volume}` | Delete the volume | `serverless:delete` |\n| GET | `/serverless/{container}/volume-backups` | List the backups, and what is on offer | `serverless:read` |\n| POST | `/serverless/{container}/volume-backups` | Take a backup | `serverless:write` |\n| GET | `/serverless/{container}/volume-backups/{backup}` | Show a backup | `serverless:read` |\n| DELETE | `/serverless/{container}/volume-backups/{backup}` | Delete a backup | `serverless:delete` |\n| POST | `/serverless/{container}/volume-backups/{backup}/restore` | Restore a backup | `serverless:write` |\n| GET | `/serverless/{container}/volume-restores` | List the last twenty restores | `serverless:read` |\n| GET | `/serverless/{container}/volume-restores/{restore}` | Show a restore | `serverless:read` |\n\nEvery 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.\n\nThe full schema of every request and response is in the [OpenAPI document](https://danubedata.ro/docs/api).\n\n## The answer\n\nEvery answer is the same envelope:\n\n```json\n{\n  \"success\": true,\n  \"data\": { \"...\": \"the thing, or a list of them\" },\n  \"error\": null,\n  \"meta\": {}\n}\n```\n\nA refusal has `\"success\": false`, `\"data\": null` and an `error`:\n\n```json\n{\n  \"success\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"serverless.volume_container_busy\",\n    \"message\": \"A change to this container is in progress. Wait for it to finish, or cancel it, and try again.\",\n    \"retryable\": true,\n    \"field\": null\n  },\n  \"meta\": {}\n}\n```\n\n- `error.code` is stable: branch on it. The message is for people and may be reworded. Every code is in [Failure codes](https://docs.danubedata.ro/failure-codes#rapids-volumes-backups-and-restores), with its HTTP status.\n- `error.retryable` says whether the same request can succeed later with nothing changed on your side.\n- `error.field` names the field of your request that a refusal is about, such as `size_gb` or `mount_path`, or is `null`.\n- 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 `422` the way the rest of the API answers validation: `{\"message\": \"...\", \"errors\": {\"size_gb\": [\"...\"]}}`.\n- A token without the ability, a container that is not in the project and a request with no token answer `401` or `403` with `{\"message\": \"...\", \"error\": \"...\"}`. A container id that does not exist answers `404` with `{\"message\": \"Not Found\", \"error\": \"Not Found\"}`.\n\n## Volumes\n\n### What is on offer\n\n```bash\ncurl 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volumes' \\\n  -H 'Authorization: Bearer YOUR_TOKEN' \\\n  -H 'Accept: application/json'\n```\n\n```json\n{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"0199d1f0-4b7e-7c3a-9a40-3f6a1c5e8b21\",\n      \"size_gb\": 10,\n      \"mount_path\": \"/data\",\n      \"status\": \"active\",\n      \"billed\": true,\n      \"monthly_cost_cents\": 120,\n      \"failure\": null,\n      \"created_at\": \"2026-10-08T09:14:22+00:00\",\n      \"updated_at\": \"2026-10-08T09:15:41+00:00\"\n    }\n  ],\n  \"error\": null,\n  \"meta\": {\n    \"total\": 1,\n    \"offer\": {\n      \"available\": true,\n      \"min_size_gb\": 1,\n      \"max_size_gb\": 50,\n      \"cents_per_gb_month\": 12,\n      \"team_limit_gb\": 100,\n      \"team_remaining_gb\": 90\n    }\n  }\n}\n```\n\nThe 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.\n\n`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.\n\n### Add a volume\n\n```bash\ncurl -X POST 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volumes' \\\n  -H 'Authorization: Bearer YOUR_TOKEN' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json' \\\n  -H 'Idempotency-Key: 5d1c0e3a-7a49-4d6c-8e0e-4b8f6a3b9c10' \\\n  -d '{\"size_gb\": 10, \"mount_path\": \"/data\"}'\n```\n\n| Field | Required | Values |\n|---|---|---|\n| `size_gb` | Yes | A whole number, 1 to 50, and no more than the project may still hold |\n| `mount_path` | Yes | An absolute path such as `/data`. See [Mount path](https://docs.danubedata.ro/serverless-volumes#mount-path) |\n\nThe 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`.\n\n### Follow a volume\n\n`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`.\n\n`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.\n\n### Make the volume larger, or move it\n\n```bash\ncurl -X PATCH 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volumes/VOLUME_ID' \\\n  -H 'Authorization: Bearer YOUR_TOKEN' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json' \\\n  -d '{\"size_gb\": 20}'\n```\n\nSend `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.\n\nA 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](https://docs.danubedata.ro/serverless-volumes#making-the-volume-larger) to use the space at once. A new mount path rolls the container out; the files do not move.\n\n### Delete a volume\n\n`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.\n\n## Backups\n\n### What is on offer\n\n`GET /serverless/{container}/volume-backups` lists the backups, newest first, with:\n\n```json\n\"meta\": {\n  \"total\": 2,\n  \"offer\": {\n    \"available\": true,\n    \"max_per_container\": 10,\n    \"cents_per_gb_month\": 7.3,\n    \"monthly_cost_cents\": 11,\n    \"stored_bytes\": 1572864000\n  }\n}\n```\n\n`stored_bytes` is what the container's backups take in the backup store now, which is what you are billed for.\n\n### Take a backup\n\n```bash\ncurl -X POST 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volume-backups' \\\n  -H 'Authorization: Bearer YOUR_TOKEN' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json' \\\n  -d '{\"name\": \"before the migration\"}'\n```\n\n`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:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"0199d1f4-0b52-7e11-8c0d-6a2d9e47b3f0\",\n    \"name\": \"before the migration\",\n    \"status\": \"pending\",\n    \"terminal\": false,\n    \"progress\": null,\n    \"size_bytes\": null,\n    \"volume_id\": \"0199d1f0-4b7e-7c3a-9a40-3f6a1c5e8b21\",\n    \"volume_size_gb\": 10,\n    \"mount_path\": \"/data\",\n    \"failure\": null,\n    \"created_at\": \"2026-10-08T10:02:09+00:00\",\n    \"ready_at\": null,\n    \"deleting_since\": null\n  },\n  \"error\": null,\n  \"meta\": { \"poll_url\": \"/api/v1/serverless/CONTAINER_ID/volume-backups/0199d1f4-0b52-7e11-8c0d-6a2d9e47b3f0\", \"poll_after_ms\": 3000 }\n}\n```\n\nPoll 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.\n\n`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.\n\nAt 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](https://docs.danubedata.ro/serverless-volumes#backups).\n\n### Delete a backup\n\n`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.\n\n## Restores\n\n### Restore a backup\n\n```bash\ncurl -X POST 'https://danubedata.ro/api/v1/serverless/CONTAINER_ID/volume-backups/BACKUP_ID/restore' \\\n  -H 'Authorization: Bearer YOUR_TOKEN' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json' \\\n  -H 'Idempotency-Key: 9a7e3b52-1c0d-4f6a-b8d4-2e5c7f1a0b63'\n```\n\nA 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.\n\nThe answer is `202` with the restore and `meta.poll_url`:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"0199d1f9-6c21-7a40-8f13-0d5b7a9e2c84\",\n    \"backup_id\": \"0199d1f4-0b52-7e11-8c0d-6a2d9e47b3f0\",\n    \"backup_name\": \"before the migration\",\n    \"status\": \"pending\",\n    \"terminal\": false,\n    \"landed\": false,\n    \"creates_volume\": false,\n    \"target_size_gb\": 10,\n    \"failure\": null,\n    \"started_at\": \"2026-10-08T10:20:31+00:00\",\n    \"finished_at\": null\n  },\n  \"error\": null,\n  \"meta\": { \"poll_url\": \"/api/v1/serverless/CONTAINER_ID/volume-restores/0199d1f9-6c21-7a40-8f13-0d5b7a9e2c84\", \"poll_after_ms\": 3000 }\n}\n```\n\nPoll 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.\n\nThe 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.\n\n## Retry safely\n\nA 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.\n\n- **Send an `Idempotency-Key`** with every write, and with `POST …/restore` above all. Retry with the same key and the same body: you get the first answer back, marked `Idempotent-Replay: true`, and nothing is started twice.\n- **Without a key**, a restore that is under way is refused with `serverless.volume_restore_in_progress`. That code is **not retryable**, and `meta` names the restore that is running (`restore_id`, `poll_url`): follow that one. Asking again once it has finished would restore a second time.\n- 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 answered `serverless.volume_restore_in_progress`, naming the failed restore.\n- 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 `409` `idempotency.key_reused`. A request that was refused does not use up its key.\n\nSee [Support Tickets API](https://docs.danubedata.ro/api-support-tickets#retry-safely) for the full rules of keys.\n\n## Limits on requests\n\nThe 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](https://docs.danubedata.ro/api-rate-limits) apply to all of them.\n\n## Errors\n\n| Status | Meaning |\n|---|---|\n| `202` | Taken. The thing is in the body; `meta.poll_url` says where to look again |\n| `200` | Read, or a change that was a repeat (`meta.changed` is `false`) |\n| `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` |\n| `404` | The container has no such volume, backup or restore: `serverless.volume_not_found`, `serverless.volume_backup_not_found`, `serverless.volume_restore_not_found` |\n| `409` | The state of the container, the volume or the backups does not allow it now. `retryable` says whether it will pass |\n| `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` |\n| `429` | More than 20 changes a minute |\n| `503` | The platform cannot do it now (no room, or it could not work out where to place the volume). Retryable |\n\nEvery `error.code` is listed in [Failure codes](https://docs.danubedata.ro/failure-codes#rapids-volumes-backups-and-restores).\n","prev":{"title":"Support Tickets","slug":"api-support-tickets","url":"https://docs.danubedata.ro/api-support-tickets","markdown_url":"https://docs.danubedata.ro/api-support-tickets.md","json_url":"https://docs.danubedata.ro/api-support-tickets.json"},"next":{"title":"Rate Limits","slug":"api-rate-limits","url":"https://docs.danubedata.ro/api-rate-limits","markdown_url":"https://docs.danubedata.ro/api-rate-limits.md","json_url":"https://docs.danubedata.ro/api-rate-limits.json"},"index_url":"https://docs.danubedata.ro/index.json"}