KV Store: atomic operations
Section titled “KV Store: atomic operations”These three endpoints mutate array values stored in the KV store in a single round trip, removing the race conditions of a read-modify-write cycle. Use push to append a value to an array, pop to remove and return the last element, and remove to drop an element either by index or by matching a value. All three endpoints accept a path query parameter that targets nested arrays by JSON path, so the same call works on top-level and nested arrays alike.
Array operations
Section titled “Array operations”POST /api/v1/sqlite/kv/{key}/push
Section titled “POST /api/v1/sqlite/kv/{key}/push”Append a value to the array stored at key. The first time the key is written with this call, the resulting array contains only the appended value.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
key | path | string | Yes | Key name |
db | query | string | Yes | Database file path |
table | query | string | No | Custom table name. Default: kv_store |
path | query | string | No | JSON path to a nested array |
history | query | boolean | No | Store this write’s value in history (false records only that the write happened, not what it wrote). Default: true |
create_db_if_missing | query | boolean | No | Create the database file if it is missing. The legacy alias auto_create is still accepted. Default: false |
timeout | query | integer | No | Request deadline in seconds, clamped to [1, 300]. Default: 30 |
If-Match | header | string | No | Applies the write only if the key’s current ETag matches. |
Idempotency-Key | header | string | No | Replays the stored response for a repeat of the same key (24 h). |
Request Body
Section titled “Request Body”The JSON body holds the value to append. Any JSON-serialisable value (object, array, string, number, boolean, or null) is accepted; an array is appended as a single element, not spread. A body that is not valid JSON is refused with a 400.
{ "id": 1, "task": "process", "created_at": "2026-01-15T10:30:00Z"}Request
Section titled “Request”curl -X POST 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/tasks/push?db=mydb.db' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "id": 1, "task": "process", "created_at": "2026-01-15T10:30:00Z" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.sqlite.kv.push('tasks', { id: 1, task: 'process', created_at: '2026-01-15T10:30:00Z'}, { db: 'mydb.db' });Response
Section titled “Response”Value appended successfully. The response includes an ETag header carrying the key’s new ETag, and an Idempotent-Replayed header set to true when this response was replayed from a stored Idempotency-Key match.
{ "etag": "9f1c2d83b7e54a08a0c1e7f3b9a2d4e1", "key": "tasks", "length": 5, "success": true}The request could not be validated, the body is not valid JSON, or the targeted value is not an array.
{ "code": "INVALID_PARAMETERS", "error": "The value at the key is not a JSON array"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETERS | Invalid request parameters | A parameter or the request body is missing, malformed or out of range. This is the default code for a 400 that has no more specific one. | Check parameter types and values against the API specification; the error text names the problem |
INVALID_DB_PATH | Invalid database path | The db/path value is empty, relative, outside /hoody/databases (unless the server allows any absolute path), contains ? or #, escapes via a symlink, or names something that is not a regular file | Provide an absolute path under /hoody/databases, or a bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
DATABASE_NOT_FOUND | Database file does not exist | The database file does not exist and this request does not create it | Check the path, create the database first, or pass create_db_if_missing=true where the operation supports it |
INVALID_SQLITE_HEADER | Not a valid SQLite database | The file exists but is empty or does not start with the SQLite header | Point db at a SQLite database file |
PATH_IS_DIRECTORY | Path is a directory | Expected a database file but the path is a directory. Only reading, writing, deleting and checking a single key and listing keys (without at_timestamp) accept a directory, given as an absolute path (directory mode) | Use a .db file path |
Change capture is not available for the target table, so this write was refused. See the resolution column for the table-shape rules that trigger this.
{ "code": "CONFLICT", "error": "The request could not be completed because it conflicts with the current state"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONFLICT | Conflict | The request conflicts with the current state. Default code for a 409 that has no more specific one | Read the current state and retry with a request that matches it |
The If-Match value did not match the key’s current ETag. The body includes current_etag with the key’s current ETag, or null when the key does not exist.
{ "code": "PRECONDITION_FAILED", "current_etag": "a1b2c3d4e5f6789012345678abcdef01", "error": "The If-Match value did not match the current ETag of the key"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PRECONDITION_FAILED | Precondition failed | The compare-and-swap value (if_match) did not match the stored value, or an If-Match / If-None-Match condition (or a batch item’s if_match / if_none_match) did not hold; current_etag (failed for a batch) carries the current ETag. Nothing was written | Read the current value and ETag and retry with them |
The request exceeded a size limit, or the response would have been too large to store for Idempotency-Key replay.
{ "code": "PAYLOAD_TOO_LARGE", "error": "The request exceeds the maximum allowed size"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request too large | The request exceeds a size or item-count limit | Split the request |
IDEMPOTENT_RESPONSE_TOO_LARGE | Response too large to store | With Idempotency-Key, the response would be larger than 1 MiB and could not be stored for replay; nothing was written | Send the request without Idempotency-Key, or narrow it |
The Idempotency-Key was reused for a different request (method, key, query, conditions, content type, or body differ).
{ "code": "IDEMPOTENCY_KEY_MISMATCH", "error": "The Idempotency-Key was reused for a different request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
IDEMPOTENCY_KEY_MISMATCH | Idempotency-Key reused | The Idempotency-Key was already used, within its lifetime, for a different request (method, key, query, conditions, content type or body differ); nothing was written | Use a new Idempotency-Key for a different request |
An unexpected error occurred while processing the request. The server log carries the full details.
{ "code": "INTERNAL_ERROR", "error": "An unexpected error occurred; please retry the request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INTERNAL_ERROR | Internal server error | An unexpected server-side failure. Default code for a 500 that has no more specific one; details are in the server log | Retry later; report it if it persists |
DATABASE_ERROR | Database fault | SQLite failed for a reason that is not the request: I/O error, corruption, disk full, a file that cannot be opened. The message is sanitised; details are in the server log | Check the server log, disk space and the database file; the database may need repair |
The database was busy, the request deadline was exceeded, or the server is shutting down. The response includes a Retry-After header set to 1.
{ "code": "DATABASE_BUSY", "error": "The database is currently busy with another operation"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SERVICE_UNAVAILABLE | Service unavailable | The service could not complete the request right now. Default code for a 503 that has no more specific one | Retry after a short delay |
REQUEST_TIMEOUT | Request deadline exceeded | The request did not finish before its deadline (the timeout parameter, default 30 s); nothing was committed | Retry, or raise timeout (max 300) |
DATABASE_BUSY | Database busy | The database was locked by another writer or in use for longer than the request could wait; nothing was changed | Retry after a short delay |
POST /api/v1/sqlite/kv/{key}/pop
Section titled “POST /api/v1/sqlite/kv/{key}/pop”Remove and return the last element of the array stored at key. Supports JSON paths for nested arrays.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
key | path | string | Yes | Key name |
db | query | string | Yes | Database file path |
table | query | string | No | Custom table name. Default: kv_store |
path | query | string | No | JSON path to a nested array |
history | query | boolean | No | Store this write’s value in history (false records only that the write happened, not what it wrote). Default: true |
create_db_if_missing | query | boolean | No | Create the database file if it is missing. The legacy alias auto_create is still accepted. Default: false |
timeout | query | integer | No | Request deadline in seconds, clamped to [1, 300]. Default: 30 |
If-Match | header | string | No | Applies the write only if the key’s current ETag matches. |
Idempotency-Key | header | string | No | Replays the stored response for a repeat of the same key (24 h). |
This endpoint accepts no request body.
Request
Section titled “Request”curl -X POST 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/tasks/pop?db=mydb.db' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.sqlite.kv.pop('tasks', { db: 'mydb.db' });Response
Section titled “Response”Value popped successfully. The popped element is returned in value along with the new array length. The response includes an ETag header carrying the key’s new ETag, and an Idempotent-Replayed header set to true when this response was replayed from a stored Idempotency-Key match.
{ "etag": "9f1c2d83b7e54a08a0c1e7f3b9a2d4e1", "key": "tasks", "length": 4, "value": { "id": 1, "task": "process", "created_at": "2026-01-15T10:30:00Z" }, "success": true}The request could not be validated, or the targeted value is not an array.
{ "code": "INVALID_PARAMETERS", "error": "The value at the key is not a JSON array"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETERS | Invalid request parameters | A parameter or the request body is missing, malformed or out of range. This is the default code for a 400 that has no more specific one. | Check parameter types and values against the API specification; the error text names the problem |
INVALID_DB_PATH | Invalid database path | The db/path value is empty, relative, outside /hoody/databases (unless the server allows any absolute path), contains ? or #, escapes via a symlink, or names something that is not a regular file | Provide an absolute path under /hoody/databases, or a bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
DATABASE_NOT_FOUND | Database file does not exist | The database file does not exist and this request does not create it | Check the path, create the database first, or pass create_db_if_missing=true where the operation supports it |
INVALID_SQLITE_HEADER | Not a valid SQLite database | The file exists but is empty or does not start with the SQLite header | Point db at a SQLite database file |
PATH_IS_DIRECTORY | Path is a directory | Expected a database file but the path is a directory. Only reading, writing, deleting and checking a single key and listing keys (without at_timestamp) accept a directory, given as an absolute path (directory mode) | Use a .db file path |
The key does not exist, or the JSON path does not exist within the stored value.
{ "code": "KEY_NOT_FOUND", "error": "The requested key does not exist in this database"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
NOT_FOUND | Not found | The requested item does not exist. Default code for a 404 that has no more specific one | Check the identifiers in the request |
KEY_NOT_FOUND | Key not found | The key does not exist (or did not exist at the requested timestamp or operation) | Verify the key name and database/table parameters |
Change capture is not available for the target table, so this write was refused. See the resolution column for the table-shape rules that trigger this.
{ "code": "CONFLICT", "error": "The request could not be completed because it conflicts with the current state"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONFLICT | Conflict | The request conflicts with the current state. Default code for a 409 that has no more specific one | Read the current state and retry with a request that matches it |
The If-Match value did not match the key’s current ETag. The body includes current_etag with the key’s current ETag, or null when the key does not exist.
{ "code": "PRECONDITION_FAILED", "current_etag": "a1b2c3d4e5f6789012345678abcdef01", "error": "The If-Match value did not match the current ETag of the key"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PRECONDITION_FAILED | Precondition failed | The compare-and-swap value (if_match) did not match the stored value, or an If-Match / If-None-Match condition (or a batch item’s if_match / if_none_match) did not hold; current_etag (failed for a batch) carries the current ETag. Nothing was written | Read the current value and ETag and retry with them |
The request exceeded a size limit, or the response would have been too large to store for Idempotency-Key replay.
{ "code": "PAYLOAD_TOO_LARGE", "error": "The request exceeds the maximum allowed size"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request too large | The request exceeds a size or item-count limit | Split the request |
IDEMPOTENT_RESPONSE_TOO_LARGE | Response too large to store | With Idempotency-Key, the response would be larger than 1 MiB and could not be stored for replay; nothing was written | Send the request without Idempotency-Key, or narrow it |
The Idempotency-Key was reused for a different request (method, key, query, conditions, content type, or body differ).
{ "code": "IDEMPOTENCY_KEY_MISMATCH", "error": "The Idempotency-Key was reused for a different request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
IDEMPOTENCY_KEY_MISMATCH | Idempotency-Key reused | The Idempotency-Key was already used, within its lifetime, for a different request (method, key, query, conditions, content type or body differ); nothing was written | Use a new Idempotency-Key for a different request |
An unexpected error occurred while processing the request. The server log carries the full details.
{ "code": "INTERNAL_ERROR", "error": "An unexpected error occurred; please retry the request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INTERNAL_ERROR | Internal server error | An unexpected server-side failure. Default code for a 500 that has no more specific one; details are in the server log | Retry later; report it if it persists |
DATABASE_ERROR | Database fault | SQLite failed for a reason that is not the request: I/O error, corruption, disk full, a file that cannot be opened. The message is sanitised; details are in the server log | Check the server log, disk space and the database file; the database may need repair |
The database was busy, the request deadline was exceeded, or the server is shutting down. The response includes a Retry-After header set to 1.
{ "code": "DATABASE_BUSY", "error": "The database is currently busy with another operation"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SERVICE_UNAVAILABLE | Service unavailable | The service could not complete the request right now. Default code for a 503 that has no more specific one | Retry after a short delay |
REQUEST_TIMEOUT | Request deadline exceeded | The request did not finish before its deadline (the timeout parameter, default 30 s); nothing was committed | Retry, or raise timeout (max 300) |
DATABASE_BUSY | Database busy | The database was locked by another writer or in use for longer than the request could wait; nothing was changed | Retry after a short delay |
POST /api/v1/sqlite/kv/{key}/remove
Section titled “POST /api/v1/sqlite/kv/{key}/remove”Remove one element from the array stored at key, either by zero-based index (via the index query parameter) or by matching a value (via the request body). When index is set, it is used and any body value is ignored for the match decision. Supports JSON paths for nested arrays.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
key | path | string | Yes | Key name |
db | query | string | Yes | Database file path |
table | query | string | No | Custom table name. Default: kv_store |
path | query | string | No | JSON path to a nested array |
index | query | integer | No | Array index to remove. Send this or a body value; with neither the request is a 400 |
history | query | boolean | No | Store this write’s value in history (false records only that the write happened, not what it wrote). Default: true |
create_db_if_missing | query | boolean | No | Create the database file if it is missing. The legacy alias auto_create is still accepted. Default: false |
timeout | query | integer | No | Request deadline in seconds, clamped to [1, 300]. Default: 30 |
If-Match | header | string | No | Applies the write only if the key’s current ETag matches. |
Idempotency-Key | header | string | No | Replays the stored response for a repeat of the same key (24 h). |
Request Body
Section titled “Request Body”Optional. When index is not supplied, the body holds a single value field and the first array element equal to that value is removed. value accepts any JSON-serialisable value (object, array, string, number, boolean, or null); an explicit null removes the first null element, which is not the same as leaving the field out.
| Field | Type | Required | Description |
|---|---|---|---|
value | any JSON value | No | The value to match and remove; required unless index is set |
{ "value": { "id": 1, "task": "process" }}Request
Section titled “Request”curl -X POST 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/tasks/remove?db=mydb.db&index=0' \ -H 'Authorization: Bearer <token>'curl -X POST 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/tasks/remove?db=mydb.db' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "value": { "id": 1, "task": "process" } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Remove by indexconst byIndex = await client.sqlite.kv.remove('tasks', {}, { db: 'mydb.db', index: 0 });
// Remove by matching valueconst byValue = await client.sqlite.kv.remove('tasks', { value: { id: 1, task: 'process' } }, { db: 'mydb.db' });Response
Section titled “Response”Element removed successfully. The response includes an ETag header carrying the key’s new ETag, and an Idempotent-Replayed header set to true when this response was replayed from a stored Idempotency-Key match.
{ "etag": "9f1c2d83b7e54a08a0c1e7f3b9a2d4e1", "key": "tasks", "length": 3, "removed": { "id": 1, "task": "process" }, "success": true}The request could not be validated, the targeted value is not an array, or neither index nor a body value was supplied.
{ "code": "INVALID_PARAMETERS", "error": "The request must include either an index query parameter or a body value"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETERS | Invalid request parameters | A parameter or the request body is missing, malformed or out of range. This is the default code for a 400 that has no more specific one. | Check parameter types and values against the API specification; the error text names the problem |
INVALID_DB_PATH | Invalid database path | The db/path value is empty, relative, outside /hoody/databases (unless the server allows any absolute path), contains ? or #, escapes via a symlink, or names something that is not a regular file | Provide an absolute path under /hoody/databases, or a bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
DATABASE_NOT_FOUND | Database file does not exist | The database file does not exist and this request does not create it | Check the path, create the database first, or pass create_db_if_missing=true where the operation supports it |
INVALID_SQLITE_HEADER | Not a valid SQLite database | The file exists but is empty or does not start with the SQLite header | Point db at a SQLite database file |
PATH_IS_DIRECTORY | Path is a directory | Expected a database file but the path is a directory. Only reading, writing, deleting and checking a single key and listing keys (without at_timestamp) accept a directory, given as an absolute path (directory mode) | Use a .db file path |
The key does not exist, the JSON path does not exist within the stored value, or no array element equals the supplied value.
{ "code": "KEY_NOT_FOUND", "error": "The requested key does not exist or no array element matches the provided value"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
NOT_FOUND | Not found | The requested item does not exist. Default code for a 404 that has no more specific one | Check the identifiers in the request |
KEY_NOT_FOUND | Key not found | The key does not exist (or did not exist at the requested timestamp or operation) | Verify the key name and database/table parameters |
Change capture is not available for the target table, so this write was refused. See the resolution column for the table-shape rules that trigger this.
{ "code": "CONFLICT", "error": "The request could not be completed because it conflicts with the current state"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONFLICT | Conflict | The request conflicts with the current state. Default code for a 409 that has no more specific one | Read the current state and retry with a request that matches it |
The If-Match value did not match the key’s current ETag. The body includes current_etag with the key’s current ETag, or null when the key does not exist.
{ "code": "PRECONDITION_FAILED", "current_etag": "a1b2c3d4e5f6789012345678abcdef01", "error": "The If-Match value did not match the current ETag of the key"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PRECONDITION_FAILED | Precondition failed | The compare-and-swap value (if_match) did not match the stored value, or an If-Match / If-None-Match condition (or a batch item’s if_match / if_none_match) did not hold; current_etag (failed for a batch) carries the current ETag. Nothing was written | Read the current value and ETag and retry with them |
The request exceeded a size limit, or the response would have been too large to store for Idempotency-Key replay.
{ "code": "PAYLOAD_TOO_LARGE", "error": "The request exceeds the maximum allowed size"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request too large | The request exceeds a size or item-count limit | Split the request |
IDEMPOTENT_RESPONSE_TOO_LARGE | Response too large to store | With Idempotency-Key, the response would be larger than 1 MiB and could not be stored for replay; nothing was written | Send the request without Idempotency-Key, or narrow it |
The Idempotency-Key was reused for a different request (method, key, query, conditions, content type, or body differ).
{ "code": "IDEMPOTENCY_KEY_MISMATCH", "error": "The Idempotency-Key was reused for a different request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
IDEMPOTENCY_KEY_MISMATCH | Idempotency-Key reused | The Idempotency-Key was already used, within its lifetime, for a different request (method, key, query, conditions, content type or body differ); nothing was written | Use a new Idempotency-Key for a different request |
An unexpected error occurred while processing the request. The server log carries the full details.
{ "code": "INTERNAL_ERROR", "error": "An unexpected error occurred; please retry the request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INTERNAL_ERROR | Internal server error | An unexpected server-side failure. Default code for a 500 that has no more specific one; details are in the server log | Retry later; report it if it persists |
DATABASE_ERROR | Database fault | SQLite failed for a reason that is not the request: I/O error, corruption, disk full, a file that cannot be opened. The message is sanitised; details are in the server log | Check the server log, disk space and the database file; the database may need repair |
The database was busy, the request deadline was exceeded, or the server is shutting down. The response includes a Retry-After header set to 1.
{ "code": "DATABASE_BUSY", "error": "The database is currently busy with another operation"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SERVICE_UNAVAILABLE | Service unavailable | The service could not complete the request right now. Default code for a 503 that has no more specific one | Retry after a short delay |
REQUEST_TIMEOUT | Request deadline exceeded | The request did not finish before its deadline (the timeout parameter, default 30 s); nothing was committed | Retry, or raise timeout (max 300) |
DATABASE_BUSY | Database busy | The database was locked by another writer or in use for longer than the request could wait; nothing was changed | Retry after a short delay |