KV Store: Time-Travel & History
Section titled “KV Store: Time-Travel & History”The KV Store time-travel API exposes the operation log that backs Hoody’s SQLite-backed key-value store. Use these endpoints to follow a table’s change feed, inspect what changed, reconstruct a key or an entire table as it was at a specific moment, diff two snapshots, and roll back unwanted mutations. Every endpoint operates against a single database file (or directory-mode table) and supports the same db and optional table query parameters used by the rest of the SQLite API.
All examples in this page use the placeholder host https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com. A real host looks like https://67e89abc123def456789abcd-890abcdef12345678901cdef-sqlite-1.node-example-1.containers.hoody.com.
Query History
Section titled “Query History”The history endpoints cover the audit trail for SQL and KV operations executed against a database file. The history table is per-database; specifying the db parameter selects which trail to read or prune.
GET /api/v1/sqlite/history
Section titled “GET /api/v1/sqlite/history”Retrieve the most recent query execution entries for a database. The server keeps the most recent 1000 entries; page through them with limit and offset, and use total to know when to stop. A transaction that failed or was rolled back is recorded too, every item with success=false: the failing item carries its own error, the others say the transaction was rolled back.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database file path |
limit | query | integer | No | Maximum number of entries to return (0 means the default; capped at 1000). Default: 100 |
offset | query | integer | No | Number of newest entries to skip. Default: 0 |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X GET "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/history?db=/hoody/databases/orders.db&limit=50" \ -H "Authorization: Bearer $HOODY_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 });
for await (const item of client.sqlite.history.listIterator({ db: '/hoody/databases/orders.db', limit: 50 })) { console.log(item);}{ "count": 2, "limit": 50, "offset": 0, "total": 1043, "entries": [ { "id": 412, "timestamp": "2024-05-28T10:42:03Z", "source": "kv", "sql": null, "success": true, "error": null, "client_ip": "10.0.0.42", "execution_time_ms": 4, "is_statement": false, "row_count": null }, { "id": 411, "timestamp": "2024-05-28T10:41:31Z", "source": "sql", "sql": "SELECT id, total FROM orders WHERE status = ?", "success": true, "error": null, "client_ip": "10.0.0.42", "execution_time_ms": 12, "is_statement": false, "row_count": 128 } ], "history": [ { "id": 412, "timestamp": "2024-05-28T10:42:03Z", "source": "kv", "sql": null, "success": true, "error": null, "client_ip": "10.0.0.42", "execution_time_ms": 4, "is_statement": false, "row_count": null }, { "id": 411, "timestamp": "2024-05-28T10:41:31Z", "source": "sql", "sql": "SELECT id, total FROM orders WHERE status = ?", "success": true, "error": null, "client_ip": "10.0.0.42", "execution_time_ms": 12, "is_statement": false, "row_count": 128 } ]}{ "code": "INVALID_DB_PATH", "error": "Invalid database path"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 |
| 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 |
GET /api/v1/sqlite/history/stats
Section titled “GET /api/v1/sqlite/history/stats”Return aggregated counters over the history table for a single database.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database file path |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X GET "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/history/stats?db=/hoody/databases/orders.db" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.history.getStats({ db: '/hoody/databases/orders.db' });{ "total_queries": 10432, "success_count": 10190, "failure_count": 242, "last_minute": 12, "last_hour": 318, "avg_exec_time_ms": 3.7}{ "code": "INVALID_DB_PATH", "error": "Invalid database path"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 |
| 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 |
DELETE /api/v1/sqlite/history
Section titled “DELETE /api/v1/sqlite/history”Delete every entry in the history table for a single database. Use with care: this is irreversible and removes the audit trail used for time-travel.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database file path |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X DELETE "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/history?db=/hoody/databases/orders.db" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.history.clear({ db: '/hoody/databases/orders.db' });{ "success": true, "message": "History cleared for /hoody/databases/orders.db"}{ "code": "INVALID_DB_PATH", "error": "Invalid database path"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 history writer could not be drained before the request deadline. Nothing was cleared. Retry the request; if it persists, raise the timeout value or wait for the writer queue to settle.
{ "code": "SERVICE_UNAVAILABLE", "error": "History writer could not be drained in time"}| 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 |
DELETE /api/v1/sqlite/history/{index}
Section titled “DELETE /api/v1/sqlite/history/{index}”Delete a single query history entry by its numeric ID.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
index | path | integer | Yes | History entry ID |
db | query | string | Yes | Database file path |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X DELETE "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/history/411?db=/hoody/databases/orders.db" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.history.delete(411, { db: '/hoody/databases/orders.db' });{ "success": true, "message": "History entry 411 deleted"}{ "code": "INVALID_DB_PATH", "error": "Invalid database path"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "HISTORY_ENTRY_NOT_FOUND", "error": "Entry not found"}| 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 |
HISTORY_ENTRY_NOT_FOUND | History entry not found | The history entry or operation number does not exist, or the key has no history to roll back. | List the history first and use an id / op_number from it |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 |
| 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 |
Key History
Section titled “Key History”Per-key history reads return the chronological log of every recorded operation against a single key. Writes made with history=false still appear as UNRECORDED entries: they record that an untracked write happened and the span of time it covers (timestamp to unrecorded_until), with no value, content type or expiry. They exist so time travel can answer “indeterminate” rather than return a stale value once such a write’s row is gone, and they count towards limit.
GET /api/v1/sqlite/kv/{key}/history
Section titled “GET /api/v1/sqlite/kv/{key}/history”Retrieve the operation history for a specific key, showing every change over time.
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" |
limit | query | integer | No | Maximum number of operations to return (0 means the default of 50, clamped to a maximum of 1000). Default: 50 |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X GET "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/session:9f2e/history?db=/hoody/databases/orders.db&limit=10" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.kv.listHistory('session:9f2e', { db: '/hoody/databases/orders.db', limit: 10 });{ "key": "session:9f2e", "operations": [ { "op_number": 412, "timestamp": 1717000123, "operation": "SET", "path": "", "delta": null, "value_preview": "{\"user\":\"9f2e\",\"cart\":[\"sku-1\",\"sku-2\"]}", "content_type": "application/json", "expires_at": null }, { "op_number": 380, "timestamp": 1716998000, "operation": "SET", "path": "", "delta": null, "value_preview": "{\"user\":\"9f2e\",\"cart\":[\"sku-1\"]}", "content_type": "application/json", "expires_at": null } ], "count": 2, "has_more": false}{ "code": "INVALID_PARAMETERS", "error": "limit must be a non-negative integer"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 |
| 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 |
Change feed
Section titled “Change feed”The change feed endpoints deliver the table’s change log to clients. Every physical change of a row is one event, whoever made it (KV API, SQL through POST /db, another process): set (written), ttl (only the expiry changed), delete, expire (an expired row was removed). Events are at-least-once and in order, so a client cache stays correct by applying them in order and re-reading values it needs, or by asking for small values inline with include_values.
Start without since: the response has no events and cursor is the current position. To build a cache, take that cursor, then list the keys (GET /kv), then follow the changes from the cursor. Continue each page from next_cursor. wait (seconds) holds a request that has no events yet until one arrives or the time is up (capped one second before the request deadline; raise it with timeout). prefix only returns keys starting with it; next_cursor still advances past other changes.
GET /api/v1/sqlite/changes
Section titled “GET /api/v1/sqlite/changes”Page through the changes made to a KV table, oldest first, or wait for the next ones (long poll).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database path |
table | query | string | No | KV table name. Default: kv_store |
since | query | string | No | Cursor to continue after (next_cursor of the previous page, or the cursor of an event). Omit to get the current cursor. |
prefix | query | string | No | Only keys starting with this prefix |
limit | query | integer | No | Maximum events in the page (1-1000, default 100). Default: 100 |
wait | query | integer | No | Seconds to wait for the first event when there is none yet (0-60, default 0). Default: 0 |
include_values | query | boolean | No | Attach the value to set/ttl events when it is still current, not expired and at most 256 KiB (otherwise value_omitted says why) |
timeout | query | integer | No | Deadline for this request, in whole seconds, clamped to [1, 300]. Once it passes, a long operation stops at its next checkpoint rather than being cut off mid-step; a step already running, such as a filesystem scan or a wait for another writer, finishes first. A request that has not finished by then answers 503 REQUEST_TIMEOUT, except that a write which has already committed still returns its success. A value that is not a whole number is ignored and the default applies. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X GET "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/changes?db=/hoody/databases/orders.db&table=kv_store&wait=10" \ -H "Authorization: Bearer $HOODY_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 });
for await (const item of client.sqlite.kv.listChangesIterator({ db: '/hoody/databases/orders.db', table: 'kv_store', wait: 10 })) { console.log(item);}{ "cursor": "c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9", "oldest_cursor": "c_eyJzZXEiOjEsInMiOiJkYXRhYmFzZTpvcmRlcnMuZGIifQ", "has_more": true, "next_cursor": "c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9", "events": [ { "cursor": "c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9", "seq": 423, "op": "set", "key": "session:9f2e", "table": "kv_store", "ts": 1717000123, "expire_at": null }, { "cursor": "c_eyJzZXEiOjQyMiwicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9", "seq": 422, "op": "expire", "key": "session:b7d1", "table": "kv_store", "ts": 1717000110, "expire_at": null } ]}{ "code": "INVALID_PARAMETERS", "error": "limit out of range"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "CONFLICT", "error": "Change capture not available for this table"}| 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 |
{ "code": "CHANGE_CURSOR_EXPIRED", "error": "Cursor no longer in the retained log", "reason": "cursor_expired"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CHANGE_CURSOR_EXPIRED | Change cursor expired | The cursor points before the oldest change the database still keeps (changes are kept 24 hours or up to 1,000,000 per database by default), so changes after it may be missing. reason is cursor_expired. | Re-read the current state (GET /kv), then follow the changes from a new cursor (GET /changes without since) |
CHANGE_CURSOR_INVALID | Change cursor no longer valid | The cursor belongs to another incarnation of the database (reason database_replaced: the file was replaced or restored, or reset_changes ran) or of the table (table_changed: dropped, renamed, or its schema changed outside the KV API), or is ahead of the change log (future_cursor). | Re-read the current state (GET /kv), then follow the changes from a new cursor (GET /changes without since) |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 is locked, the request deadline elapsed, or too many change streams are open. The response includes a Retry-After header (seconds to wait, always 1). Retry the request after the delay.
{ "code": "DATABASE_BUSY", "error": "Database is busy"}| 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 |
GET /api/v1/sqlite/changes/stream
Section titled “GET /api/v1/sqlite/changes/stream”Server-Sent Events stream of the changes made to a KV table (see GET /changes for the events and cursors).
Frames: retry: 1000, then event: ready (id: = the cursor the stream continues from, data {"cursor","oldest_cursor"}); event: change for each matching change (id: = its cursor, data = the event); event: checkpoint (id: = the position scanned, data {"cursor"}) every 15 s and after 1000 skipped changes, so a reconnect skips them; and, before the server closes the stream, event: reset (data {"reason"}: cursor_expired, database_replaced, table_changed, future_cursor, re-read the state and start again) or event: end (data {"reason"}: database_deleted, shutdown, max_lifetime, reconnect with the last id for shutdown and max_lifetime).
Resume with Last-Event-ID (the last id: received; wins over since). Without either, the stream starts at the current position. An invalid cursor at the start is a JSON 410 instead of a stream. A stream lasts at most one hour (server setting); the request timeout does not apply. The number of open streams is capped server-wide: over the cap the answer is 503 with Retry-After. Not available in directory mode.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database path |
table | query | string | No | KV table name. Default: kv_store |
since | query | string | No | Cursor to continue after; Last-Event-ID wins when both are sent |
prefix | query | string | No | Only keys starting with this prefix |
include_values | query | boolean | No | Attach small current values to set/ttl events (see GET /changes) |
Last-Event-ID | header | string | No | Resumes the stream after this event id. |
curl -N "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/changes/stream?db=/hoody/databases/orders.db&table=kv_store" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.kv.streamChanges({ db: '/hoody/databases/orders.db', table: 'kv_store' });The response body is a text/event-stream of Server-Sent Events. Each frame is one of ready, change, checkpoint, reset, or end. The named events and their data are described in the endpoint overview above.
retry: 1000
event: readyid: c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9data: {"cursor":"c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9","oldest_cursor":"c_eyJzZXEiOjEsInMiOiJkYXRhYmFzZTpvcmRlcnMuZGIifQ"}
event: changeid: c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9data: {"cursor":"c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9","seq":423,"op":"set","key":"session:9f2e","table":"kv_store","ts":1717000123,"expire_at":null}
event: checkpointid: c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9data: {"cursor":"c_eyJzZXEiOjQyMywicyI6ImRhdGFiYXNlOm9yZGVycy5kYiJ9"}{ "code": "INVALID_PARAMETERS", "error": "include_values must be a boolean"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "CONFLICT", "error": "Change capture not available for this table"}| 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 |
{ "code": "CHANGE_CURSOR_INVALID", "error": "Cursor belongs to a replaced database", "reason": "database_replaced"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CHANGE_CURSOR_EXPIRED | Change cursor expired | The cursor points before the oldest change the database still keeps (changes are kept 24 hours or up to 1,000,000 per database by default), so changes after it may be missing. reason is cursor_expired. | Re-read the current state (GET /kv), then follow the changes from a new cursor (GET /changes without since) |
CHANGE_CURSOR_INVALID | Change cursor no longer valid | The cursor belongs to another incarnation of the database (reason database_replaced: the file was replaced or restored, or reset_changes ran) or of the table (table_changed: dropped, renamed, or its schema changed outside the KV API), or is ahead of the change log (future_cursor). | Re-read the current state (GET /kv), then follow the changes from a new cursor (GET /changes without since) |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 |
Too many open streams, the server is shutting down, or the database is busy. The response includes a Retry-After header (seconds to wait, always 1). Retry after the delay.
{ "code": "SERVICE_UNAVAILABLE", "error": "Too many open change streams"}| 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 |
Snapshots and Diffs
Section titled “Snapshots and Diffs”These endpoints reconstruct past state from the operation log. A key snapshot rebuilds a single key’s value at a chosen operation number. A table snapshot rebuilds the whole KV table at a chosen Unix timestamp and supports prefix filtering to narrow large reconstructions. The diff endpoint compares two timestamps and reports created, modified, and deleted keys.
GET /api/v1/sqlite/kv/{key}/snapshot
Section titled “GET /api/v1/sqlite/kv/{key}/snapshot”Reconstruct a single key’s value as it was at a specific operation number.
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" |
op_number | query | integer | Yes | Operation number to reconstruct from |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X GET "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/session:9f2e/snapshot?db=/hoody/databases/orders.db&op_number=380" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.kv.getSnapshot('session:9f2e', { db: '/hoody/databases/orders.db', op_number: 380 });{ "success": true, "op_number": 380, "content_type": "application/json", "value": {"user": "9f2e", "cart": ["sku-1", "sku-2"]}}{ "code": "INVALID_PARAMETERS", "error": "op_number must be a non-negative integer"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
The requested operation number, key, or database does not exist.
{ "code": "KEY_NOT_FOUND", "error": "Key not found"}| 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 / operation). | Verify the key name and database/table parameters |
HISTORY_ENTRY_NOT_FOUND | History entry not found | The history entry or operation number does not exist, or the key has no history to roll back. | List the history first and use an id / op_number from it |
The operation number names an UNRECORDED entry, a write made with history=false. The history records that an untracked write happened but not what it wrote, so there is no value to reconstruct. Pick an earlier or later recorded operation number, or rewrite the key with history tracking enabled to restore determinism.
{ "code": "TIME_TRAVEL_CHAIN_GAP", "error": "Cannot reconstruct value at UNRECORDED operation"}| 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 |
TIME_TRAVEL_CHAIN_GAP | History has a gap | A write made with history=false (or a pruned history entry) sits in the range, so the value at that point cannot be reconstructed. | Pick a point after the gap; writes made with history=false cannot be reconstructed |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 |
| 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 |
The key’s history holds more than 50000 operations, too many to reconstruct.
{ "code": "PAYLOAD_TOO_LARGE", "error": "History too large to reconstruct"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request too large | The request exceeds a size or item-count limit. | Split the request |
GET /api/v1/sqlite/kv/snapshot
Section titled “GET /api/v1/sqlite/kv/snapshot”Reconstruct the entire KV table as it was at a given Unix timestamp.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database file path |
table | query | string | No | Custom table name. Default: "kv_store" |
timestamp | query | integer | Yes | Unix timestamp to reconstruct from |
limit | query | integer | No | Maximum number of keys to return. Default: 100 |
prefix | query | string | No | Filter keys by prefix |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X GET "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/snapshot?db=/hoody/databases/orders.db×tamp=1716900000&prefix=session:&limit=50" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.kv.getTableSnapshot({ db: '/hoody/databases/orders.db', timestamp: 1716900000, prefix: 'session:', limit: 50 });{ "success": true, "timestamp": 1716900000, "snapshot_time": "2024-05-28T08:00:00Z", "keys": [ { "key": "session:9f2e", "content_type": "application/json", "op_number": 380, "value": {"user": "9f2e", "cart": ["sku-1"]} }, { "key": "session:a31c", "content_type": "application/json", "op_number": 412, "value": {"user": "a31c", "cart": []} } ], "count": 2, "total_keys_at_snapshot": 2, "has_gaps": false, "gap_keys": null, "candidate_truncated": false}{ "code": "INVALID_DB_PATH", "error": "Invalid database path"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 request deadline elapsed before the snapshot could be produced. The candidate set was too large or maintenance contention was high. Narrow the request with prefix and/or a smaller limit and retry.
{ "code": "REQUEST_TIMEOUT", "error": "Request deadline exceeded before commit"}| 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 |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request too large | The request exceeds a size or item-count limit | Split the request |
GET /api/v1/sqlite/kv/diff
Section titled “GET /api/v1/sqlite/kv/diff”Compare the KV table state between two Unix timestamps. The response reports keys that were created, modified, and deleted in the window.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database file path |
table | query | string | No | Custom table name. Default: "kv_store" |
from | query | integer | Yes | Starting timestamp (Unix) |
to | query | integer | Yes | Ending timestamp (Unix) |
keys | query | string | No | Comma-separated list of keys to compare (optional). When given, exactly these keys are reconstructed (duplicates collapsed). |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
curl -X GET "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/diff?db=/hoody/databases/orders.db&from=1716800000&to=1717000123&keys=session:9f2e,session:a31c" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.kv.compareTableSnapshots({ db: '/hoody/databases/orders.db', from: 1716800000, to: 1717000123, keys: 'session:9f2e,session:a31c' });{ "success": true, "from_timestamp": 1716800000, "to_timestamp": 1717000123, "changes": [ { "change_type": "created", "key": "session:a31c", "to_content_type": "application/json", "to_value": {"user": "a31c", "cart": []} }, { "change_type": "modified", "key": "session:9f2e", "from_content_type": "application/json", "from_value": {"user": "9f2e", "cart": ["sku-1"]}, "to_content_type": "application/json", "to_value": {"user": "9f2e", "cart": ["sku-1", "sku-2"]}, "operations_between": [ { "op_number": 380, "operation": "SET", "path": "", "timestamp": 1716998000, "delta": null } ] } ], "stats": { "created": 1, "deleted": 0, "modified": 1, "unchanged": 0, "total_changes": 2 }, "has_gaps": false, "gap_keys": null, "candidate_truncated": false}{ "code": "INVALID_PARAMETERS", "error": "from must be <= to"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 request deadline elapsed before the diff could be produced. The candidate set was too large or maintenance contention was high. Narrow the request with keys or prefix and retry.
{ "code": "REQUEST_TIMEOUT", "error": "Request deadline exceeded before commit"}| 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 |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request too large | The request exceeds a size or item-count limit | Split the request |
Rollback
Section titled “Rollback”The rollback endpoints revert KV state using the same operation log that powers the time-travel reads. A key-level rollback undoes the most recent steps operations for one key. A table-level rollback rewinds the entire table to a target timestamp and supports a dry-run preview.
POST /api/v1/sqlite/kv/{key}/rollback
Section titled “POST /api/v1/sqlite/kv/{key}/rollback”Undo the last N operations on a single key by replaying the inverse operations from the history log.
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" |
steps | query | integer | No | Number of operations to rollback. Default: 1 |
create_db_if_missing | query | boolean | No | Create database file if it is missing. The legacy alias auto_create is still accepted and means the same thing; the two must agree when both are sent. Default: false |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
Idempotency-Key | header | string | No | Replays the stored response for a repeat of the same key (24 h). |
curl -X POST "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/session:9f2e/rollback?db=/hoody/databases/orders.db&steps=1" \ -H "Authorization: Bearer $HOODY_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 });
await client.sqlite.kv.rollback('session:9f2e', { db: '/hoody/databases/orders.db', steps: 1 });{ "key": "session:9f2e", "rolled_back": 1, "success": true}{ "code": "INVALID_PARAMETERS", "error": "steps must be >= 1"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
{ "code": "HISTORY_ENTRY_NOT_FOUND", "error": "No history found for key"}| 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 |
HISTORY_ENTRY_NOT_FOUND | History entry not found | The history entry or operation number does not exist, or the key has no history to roll back. | List the history first and use an id / op_number from it |
A write made with history=false sits in the range, so the earlier value cannot be reconstructed.
{ "code": "TIME_TRAVEL_CHAIN_GAP", "error": "Cannot rollback past an UNRECORDED write"}| 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 |
TIME_TRAVEL_CHAIN_GAP | History has a gap | A write made with history=false (or a pruned history entry) sits in the range, so the value at that point cannot be reconstructed. | Pick a point after the gap; writes made with history=false cannot be reconstructed |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 |
| 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 |
| 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 |
| 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 |
POST /api/v1/sqlite/kv/rollback
Section titled “POST /api/v1/sqlite/kv/rollback”Roll the entire KV table back to a target Unix timestamp. Use dry_run=true first to preview the set of keys that would change, then re-issue with confirm=yes to apply.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database file path |
table | query | string | No | Custom table name. Default: "kv_store" |
to_timestamp | query | integer | Yes | Target timestamp to rollback to (Unix) |
dry_run | query | boolean | No | Preview changes without applying. Default: false |
confirm | query | string | No | Must be 'yes' to execute actual rollback |
create_db_if_missing | query | boolean | No | Create database file if it is missing. The legacy alias auto_create is still accepted and means the same thing; the two must agree when both are sent. Default: false |
timeout | query | integer | No | Deadline for this request, in seconds, clamped to [1, 300]. It sets the request context deadline that handlers check between steps, so a long operation stops at the next checkpoint rather than being cut off mid-step; work already in progress, such as a filesystem scan or a wait for another writer, runs to completion. This is a server-side deadline, not a client transport timeout. Omitted, the server default applies (30 seconds unless the deployment overrides it). Default: 30 |
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 filters applied to the rollback. Omit the body to roll back the whole table.
| Name | Type | Required | Description |
|---|---|---|---|
keys | array of string | No | Keys to roll back. Omit or leave empty to roll back the whole table. At most 10000 entries; more is rejected with 413. |
exclude_keys | array of string | No | Keys to leave untouched. Applied after keys. At most 10000 entries; more is rejected with 413. |
curl -X POST "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/rollback?db=/hoody/databases/orders.db&to_timestamp=1716900000&dry_run=true" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"keys":["session:9f2e","session:a31c"]}'curl -X POST "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv/rollback?db=/hoody/databases/orders.db&to_timestamp=1716900000&confirm=yes" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.sqlite.kv.rollbackTable( { keys: ['session:9f2e', 'session:a31c'] }, { db: '/hoody/databases/orders.db', to_timestamp: 1716900000, dry_run: true });{ "success": true, "dry_run": true, "to_timestamp": 1716900000, "preview": { "total_keys": 2, "keys_to_rollback": 1, "keys_to_restore": 1, "keys_to_delete": 0, "operations_to_reverse": 2, "discovery_truncated": false, "changes": [ { "action": "rollback", "key": "session:9f2e", "operations_to_undo": 1, "current_content_type": "application/json", "current_value": {"user": "9f2e", "cart": ["sku-1", "sku-2"]}, "rollback_content_type": "application/json", "rollback_value": {"user": "9f2e", "cart": ["sku-1"]} }, { "action": "restore", "key": "session:a31c", "operations_to_undo": 0, "rollback_content_type": "application/json", "rollback_value": {"user": "a31c", "cart": []} } ], "untouched_keys": [] }}{ "code": "INVALID_PARAMETERS", "error": "Missing confirm=yes"}| 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 the KV endpoints accept a directory (directory mode). | Use a .db file path |
The time-travel chain has a gap that prevents a deterministic rollback, or change capture is not available for this table (a trigger that names the table, a self-referencing foreign key with CASCADE / SET NULL / SET DEFAULT, a UNIQUE index made only of expressions, or a rowid table whose columns hide rowid, _rowid_ and oid). Resolve by performing a full rewrite of the affected keys (read current state at to_timestamp and write the value forward), then resume history.
{ "code": "TIME_TRAVEL_CHAIN_GAP", "error": "Time-travel chain gap: cannot rollback deterministically"}| 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 |
TIME_TRAVEL_CHAIN_GAP | History has a gap | A write made with history=false (or a pruned history entry) sits in the range, so the value at that point cannot be reconstructed. | Pick a point after the gap; writes made with history=false cannot be reconstructed |
The keys or exclude_keys array exceeds the 10000-entry cap, a whole-table rollback could not discover every key in the table, a key’s history holds more than 50000 operations, or, with Idempotency-Key, the response is larger than 1 MiB and cannot be stored. Narrow the request with an explicit keys list (under 10000 entries) and retry, or remove the Idempotency-Key.
{ "code": "PAYLOAD_TOO_LARGE", "error": "keys array exceeds 10000 entries"}| 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 |
{ "code": "INTERNAL_ERROR", "error": "Internal server error"}| 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 |
| 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 |
| 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 |