## Add a row

`POST /v1/infra/servers/:id/db/tables/:table/rows`

Adds one row to a table in the SQLite database of a Galaxy application. Column values are passed in the request body; no SQL is needed.

Before the write, the platform saves a protective backup of the database — see [Protective backups before writes](/docs/infra/database#protective-backups-before-writes). Access conditions — [When the database is available](/docs/infra/database#when-the-database-is-available).

## Parameters

| Parameter | Type | Required | Description |
|----------|-----|:-----:|---------|
| `id` (path) | string | yes | ID of an application with `kind` equal to `GALAXY_APP`. List: [`GET /v1/infra/servers`](/docs/infra/servers/list) |
| `table` (path) | string | yes | Exact table name from `data.tables[].name` in the [`GET /db/schema`](/docs/infra/database/schema) response, up to 256 bytes in UTF-8. The table must have `editability.editable: true`: views, virtual tables and tables with neither a primary key nor an available `rowid` do not accept rows |

## Request fields (body)

The body is strict JSON: fields not listed in the table below are rejected with `400 VALIDATION_ERROR`. The body is limited to 64 KiB.

| Field | Type | Required | Description |
|------|-----|:-----:|---------|
| `file` | string | no | Relative path to the database file inside the application volume, up to 1024 bytes in UTF-8. Defaults to the `VIBE_DB_PATH` value of the application container, or `app.db` if it is not set. Absolute paths, parent-directory traversal and symbolic links are not allowed |
| `values` | object | yes | Values of the new row: the key is a column name from the schema, up to 256 bytes in UTF-8, the value is the cell. Up to 256 columns. An empty object `{}` adds a row of default values — `INSERT ... DEFAULT VALUES`. Generated and hidden columns cannot be passed |
| `values.<column>` | null \| number \| string \| object | — | `null`, a finite number, a string up to 64 KiB in UTF-8 or an object `{ "type": "integer", "value": "9223372036854775807" }` for an exact 64-bit integer. An integer without the object is accepted only within ±9007199254740991. Booleans, arrays, BLOBs and other objects are not accepted |

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/7c2b1f08-3a4d-4e91-9b6c-2f5e8a1d0c33/db/tables/customers/rows" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "values": {
      "name": "Customer D",
      "email": "d@example.com"
    }
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/7c2b1f08-3a4d-4e91-9b6c-2f5e8a1d0c33/db/tables/customers/rows" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "values": {
      "name": "Customer D",
      "email": "d@example.com"
    }
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/infra/servers/7c2b1f08-3a4d-4e91-9b6c-2f5e8a1d0c33/db/tables/customers/rows', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    values: {
      name: 'Customer D',
      email: 'd@example.com',
    },
  }),
})

const { success, data, error } = await res.json()
if (res.status === 409 && error.code === 'APP_DB_BACKUP_NOT_READY') {
  // The protective backup is being prepared — retry after Retry-After seconds
  console.log('Retry in', res.headers.get('Retry-After'), 's')
} else {
  console.log('Protective backup:', data.backupId)
}
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/infra/servers/7c2b1f08-3a4d-4e91-9b6c-2f5e8a1d0c33/db/tables/customers/rows', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    values: {
      name: 'Customer D',
      email: 'd@example.com',
    },
  }),
})

const { success, data, error } = await res.json()
if (res.status === 409 && error.code === 'APP_DB_BACKUP_NOT_READY') {
  // The protective backup is being prepared — retry after Retry-After seconds
  console.log('Retry in', res.headers.get('Retry-After'), 's')
} else {
  console.log('Protective backup:', data.backupId)
}
```

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | Always `true` on success |
| `data.file` | string | Database file the row was added to |
| `data.table` | string | Table name |
| `data.rowCount` | number | Number of rows added, always `1` |
| `data.durationMs` | number | Write execution time in the database, in milliseconds |
| `data.backupId` | string | ID of the protective backup taken before the first row edit of the last 10 minutes. Row edits in this window share one backup, and restoring it undoes all of them. Backup list — [`GET /db/backups`](/docs/infra/database/backups), restore — [`POST /db/restore`](/docs/infra/database/restore) |

## Response example

```json
{
  "success": true,
  "data": {
    "file": "app.db",
    "rowCount": 1,
    "durationMs": 0.903,
    "table": "customers",
    "backupId": "cb2fcdfd-bace-4932-a276-31d737337f3d"
  }
}
```

## Error response example

409 — the protective backup is not ready yet; the row was not added. The response comes with the `Retry-After: 1` header:

```json
{
  "success": false,
  "error": {
    "code": "APP_DB_BACKUP_NOT_READY",
    "message": "Database backup is not ready",
    "details": {
      "backupId": "cb2fcdfd-bace-4932-a276-31d737337f3d",
      "retryable": true,
      "retryAfterSeconds": 1
    }
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 409 | `APP_DB_BACKUP_NOT_READY` | The protective backup is being prepared; the row was not added. `error.details` contains `backupId`, `retryable: true` and `retryAfterSeconds`; the `Retry-After` header carries the same seconds, from 1 to 60. Retry the same request after the pause |
| 409 | `APP_DB_ROW_NOT_EDITABLE` | The table does not accept rows: a view, a virtual table or a table without an available key. Also returned when a column from `values` does not exist in the table or is generated |
| 409 | `APP_DB_BUSY` | A deploy, migration or restore of the application or another database write is in progress, or waking the application or host is blocked. The `Retry-After` header is present |
| 409 | `APP_DB_WAL_CLOSED` | The protective backup was not created: a running application has closed its database connection in WAL mode. The row was not added; there is no `Retry-After` header. Configure the application to keep the connection open |
| 400 | `VALIDATION_ERROR` | The body failed validation: `values` is missing, an extra field is present, a value type or `file` is invalid, or the table name exceeds 256 bytes |
| 400 | `APP_DB_INVALID_FILE` | The `file` path does not point to a valid database file inside `/data` |
| 400 | `APP_DB_UNSUPPORTED_PLACEMENT` | The application is not hosted in Galaxy |
| 404 | `APP_DB_TABLE_NOT_FOUND` | The database has no `:table` table |
| 404 | `APP_DB_NOT_FOUND` | There is no volume or database file: the application has not created a database yet |
| 404 | `NOT_FOUND` | The application was not found, was deleted or is not accessible to this key |
| 413 | `PAYLOAD_TOO_LARGE` | The body exceeds 64 KiB or the full input of the service process exceeds 96 KiB |
| 422 | `APP_DB_SQL_ERROR` | The database rejected the insert; the transaction was rolled back. The cause is in `error.details.reason`, for example `constraint_failed` or `type_mismatch` |
| 422 | `APP_DB_QUERY_TIMEOUT` | The write did not finish within 30 seconds; the transaction was rolled back |
| 422 | `APP_DB_CORRUPT` | The database file is corrupted |
| 422 | `APP_DB_BACKUP_TIMEOUT` | The protective backup did not finish within the allotted time; the row was not added |
| 422 | `APP_DB_BACKUP_INVALID` | The protective backup failed validation; the row was not added |
| 503 | `APP_DB_PRE_CHANGE_FAILED` | The protective backup was not created; the row was not added. The error is reported once — the next call starts a new backup |
| 503 | `APP_DB_WRITE_OUTCOME_UNKNOWN` | The database response was lost; the row may have been added. `retryable: false` — do not retry automatically |
| 503 | `APP_DB_TOOL_UNAVAILABLE` | The host is not prepared or is temporarily unavailable. The `Retry-After` header is present |
| 402 | `BILLING_FROZEN` | The portal's billing is frozen |
| 402 | `BILLING_INSUFFICIENT` | The portal balance is insufficient to create a protective backup |
| 403 | `APP_DB_READONLY_KEY` | A read-only key cannot change the database |
| 403 | `APP_DB_DISABLED` | Database access is not enabled for the portal |
| 403 | `INFRA_SCOPE_REQUIRED` | The key lacks the `vibe:infra` scope |
| 429 | `RATE_LIMITED` | The write request limit for the portal was exceeded — [Limits](/docs/infra/database#limits). The pause is in the `Retry-After` header |
| 401 | `MISSING_API_KEY` | `X-Api-Key` is missing |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

**The new row's key is not returned.** The response contains neither the values nor the key of the added row. To update or delete it, read the rows through [`GET /db/tables/:table/rows`](/docs/infra/database/rows) and take the key from `rowKeys`.

**A retry after an unknown outcome may add the row twice.** There is no idempotency key. After `503 APP_DB_WRITE_OUTCOME_UNKNOWN`, check the table rows first and only then send the request again.

## See also

- [Update a row](/docs/infra/database/rows-update)
- [Delete a row](/docs/infra/database/rows-delete)
- [Get table rows](/docs/infra/database/rows)
- [Get the database schema](/docs/infra/database/schema)
- [Run write SQL](/docs/infra/database/execute)
- [Restore the database from a backup](/docs/infra/database/restore)
- [Application database](/docs/infra/database)
