> For the complete documentation index, see [llms.txt](https://docs.ninox.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ninox.com/ninox-api/de/api-reference/api-endpoints/records.md).

# Records

## Get records from a table

> Retrieves paginated rows from a table with optional filtering and sorting. \`choice\` and \`multi\` values are returned as option ids (an array of ids for \`multi\`), the same value a script reads. A filter on a choice field accepts an option id or a label.

```json
{"openapi":"3.0.0","info":{"title":"Ninox Public API","version":"1.0.0"},"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"API Key","description":"Enter your Workspace API key. \n\n API keys can be generated and managed within the **Workspace Integration** settings in the Ninox app.","in":"header","name":"Authorization","type":"http"}},"schemas":{"RowsResponse":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"values":{"type":"object","additionalProperties":{"type":["boolean","null","number","string"]}}},"required":["id","values"]},"description":"List of rows"},"meta":{"type":"object","properties":{"filter":{"type":"object","additionalProperties":{"type":["boolean","null","number","string"]},"description":"Applied filter conditions"},"sort":{"type":"string","description":"Applied sort field"}}},"page_info":{"type":"object","properties":{"has_more":{"type":"boolean","description":"Whether more results are available"},"limit":{"type":"number","description":"Maximum results per page"},"offset":{"type":"number","description":"Current offset"}},"description":"Pagination information"}},"required":["data","page_info"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}},"required":["error"]}}},"paths":{"/api/v1/workspace/{workspaceId}/modules/{moduleName}/tables/{tableName}/records":{"get":{"operationId":"RowsV1Controller_getRows","summary":"Get records from a table","description":"Retrieves paginated rows from a table with optional filtering and sorting. `choice` and `multi` values are returned as option ids (an array of ids for `multi`), the same value a script reads. A filter on a choice field accepts an option id or a label.","parameters":[{"name":"moduleName","required":true,"in":"path","description":"Module name","schema":{"minLength":1,"type":"string"}},{"name":"tableName","required":true,"in":"path","description":"Table name","schema":{"minLength":1,"type":"string"}},{"name":"workspaceId","required":true,"in":"path","description":"Workspace ID","schema":{"minLength":12,"maxLength":12,"pattern":"^[a-z0-9]+$","type":"string"}},{"name":"fields","required":false,"in":"query","description":"Comma-separated list of field names to include","schema":{"type":"string"}},{"name":"filter","required":false,"in":"query","description":"Filter conditions as a JSON string (e.g., {\"email\": \"test@example.com\"})","schema":{"type":"string"}},{"name":"filters","required":false,"in":"query","description":"Deprecated alias for `filter`, from Ninox 3. Use `filter` instead.","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of rows to return (1-100)","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"offset","required":false,"in":"query","description":"Number of rows to skip (starts with 0)","schema":{"minimum":0,"type":"number"}},{"name":"choiceStyle","required":false,"in":"query","not":{},"schema":{}},{"name":"dateStyle","required":false,"in":"query","not":{},"schema":{}},{"name":"style","required":false,"in":"query","not":{},"schema":{}},{"name":"sort","required":false,"in":"query","description":"Comma-separated field names to sort by; prefix a field with \"-\" for descending, e.g. sort=name,-_md","schema":{"type":"string"}}],"responses":{"200":{"description":"Rows retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RowsResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Table not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Records"]}}}}
```

## Create records in a table

> Creates new records in an existing table (max. 6MB payload size). Values follow the same rules as in the update endpoint. For example an \`icon\` field takes an icon name or an object \`{"icon": "check\_circle", "color": "#4CAF50", "filling": 1}\`.

```json
{"openapi":"3.0.0","info":{"title":"Ninox Public API","version":"1.0.0"},"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"API Key","description":"Enter your Workspace API key. \n\n API keys can be generated and managed within the **Workspace Integration** settings in the Ninox app.","in":"header","name":"Authorization","type":"http"}},"schemas":{"AddRowsBody":{"type":"object","properties":{"records":{"minItems":1,"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Array of records to add to the target table"}},"required":["records"],"additionalProperties":false},"CreateRowsResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Created row ids"}}}},"required":["data"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}},"required":["error"]}}},"paths":{"/api/v1/workspace/{workspaceId}/modules/{moduleName}/tables/{tableName}/records":{"post":{"operationId":"RowsV1Controller_addRows","summary":"Create records in a table","description":"Creates new records in an existing table (max. 6MB payload size). Values follow the same rules as in the update endpoint. For example an `icon` field takes an icon name or an object `{\"icon\": \"check_circle\", \"color\": \"#4CAF50\", \"filling\": 1}`.","parameters":[{"name":"moduleName","required":true,"in":"path","description":"Module name","schema":{"minLength":1,"type":"string"}},{"name":"tableName","required":true,"in":"path","description":"Table name","schema":{"minLength":1,"type":"string"}},{"name":"workspaceId","required":true,"in":"path","description":"Workspace ID","schema":{"minLength":12,"maxLength":12,"pattern":"^[a-z0-9]+$","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRowsBody"}}}},"responses":{"201":{"description":"Records created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRowsResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Records"]}}}}
```

## Delete records from a table

> Deletes existing records from a table in a single transaction. If any record id is invalid, does not exist, or deletion fails, all changes are rolled back.

```json
{"openapi":"3.0.0","info":{"title":"Ninox Public API","version":"1.0.0"},"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"API Key","description":"Enter your Workspace API key. \n\n API keys can be generated and managed within the **Workspace Integration** settings in the Ninox app.","in":"header","name":"Authorization","type":"http"}},"schemas":{"DeleteRowsBody":{"type":"object","properties":{"records":{"minItems":1,"type":"array","items":{"type":"integer","exclusiveMinimum":true,"maximum":9007199254740991,"minimum":0},"description":"Array of record ids to delete"}},"required":["records"],"additionalProperties":false},"DeleteRowsResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"deletedIds":{"type":"array","items":{"type":"string"},"description":"Deleted row ids"}}}},"required":["data"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}},"required":["error"]}}},"paths":{"/api/v1/workspace/{workspaceId}/modules/{moduleName}/tables/{tableName}/records":{"delete":{"operationId":"RowsV1Controller_deleteRows","summary":"Delete records from a table","description":"Deletes existing records from a table in a single transaction. If any record id is invalid, does not exist, or deletion fails, all changes are rolled back.","parameters":[{"name":"moduleName","required":true,"in":"path","description":"Module name","schema":{"minLength":1,"type":"string"}},{"name":"tableName","required":true,"in":"path","description":"Table name","schema":{"minLength":1,"type":"string"}},{"name":"workspaceId","required":true,"in":"path","description":"Workspace ID","schema":{"minLength":12,"maxLength":12,"pattern":"^[a-z0-9]+$","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteRowsBody"}}}},"responses":{"200":{"description":"Records deleted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteRowsResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found (workspace, module, table, or record)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Records"]}}}}
```

## Update records in a table

> Updates existing records in a table (max. 6MB payload size). Each record must include an \`id\` field referencing an existing record. The update is transactional — if any record fails validation or does not exist, all changes are rolled back.\
> \
> \*\*Field type rules:\*\*\
> \- \`multi\` — must be an \*\*array of strings\*\* (e.g. \`\["Gold", "Silver"]\`). A plain string is not accepted.\
> \- \`choice\` — a single string (e.g. \`"Gold"\`) or an array of strings.\
> \- \`icon\` — an icon name (e.g. \`"check\_circle"\`) or an object \`{"icon": "check\_circle", "color": "#4CAF50", "filling": 1}\`. Any other value is refused with a 400.\
> \- \`choice\` and \`multi\` values are written as the option \*\*id\*\* or an option label. Reads always return the option \*\*id\*\*, the same value a script reads. The field definition lists each option with its id and labels.\
> \- \`boolean\` — \`true\` or \`false\` only; numeric \`1\`/\`0\` are not accepted.\
> \- \`number\` — a finite number or a numeric string (e.g. \`"42.5"\`).\
> \- \`date\` / \`timestamp\` / \`appointment\` — ISO 8601 string or Unix timestamp (ms).\
> \- Any field may be set to \`null\` to clear its value.

```json
{"openapi":"3.0.0","info":{"title":"Ninox Public API","version":"1.0.0"},"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"API Key","description":"Enter your Workspace API key. \n\n API keys can be generated and managed within the **Workspace Integration** settings in the Ninox app.","in":"header","name":"Authorization","type":"http"}},"schemas":{"UpdateRowsBody":{"type":"object","properties":{"records":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","exclusiveMinimum":true,"maximum":9007199254740991,"minimum":0}},"required":["id"],"additionalProperties":{}},"description":"Array of records to update (each must include an id field as a positive integer)"}},"required":["records"],"additionalProperties":false},"UpdateRowsResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"updatedIds":{"type":"array","items":{"type":"string"},"description":"Updated row ids"}}}},"required":["data"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}},"required":["error"]}}},"paths":{"/api/v1/workspace/{workspaceId}/modules/{moduleName}/tables/{tableName}/records":{"patch":{"operationId":"RowsV1Controller_updateRows","summary":"Update records in a table","description":"Updates existing records in a table (max. 6MB payload size). Each record must include an `id` field referencing an existing record. The update is transactional — if any record fails validation or does not exist, all changes are rolled back.\n\n**Field type rules:**\n- `multi` — must be an **array of strings** (e.g. `[\"Gold\", \"Silver\"]`). A plain string is not accepted.\n- `choice` — a single string (e.g. `\"Gold\"`) or an array of strings.\n- `icon` — an icon name (e.g. `\"check_circle\"`) or an object `{\"icon\": \"check_circle\", \"color\": \"#4CAF50\", \"filling\": 1}`. Any other value is refused with a 400.\n- `choice` and `multi` values are written as the option **id** or an option label. Reads always return the option **id**, the same value a script reads. The field definition lists each option with its id and labels.\n- `boolean` — `true` or `false` only; numeric `1`/`0` are not accepted.\n- `number` — a finite number or a numeric string (e.g. `\"42.5\"`).\n- `date` / `timestamp` / `appointment` — ISO 8601 string or Unix timestamp (ms).\n- Any field may be set to `null` to clear its value.","parameters":[{"name":"moduleName","required":true,"in":"path","description":"Module name","schema":{"minLength":1,"type":"string"}},{"name":"tableName","required":true,"in":"path","description":"Table name","schema":{"minLength":1,"type":"string"}},{"name":"workspaceId","required":true,"in":"path","description":"Workspace ID","schema":{"minLength":12,"maxLength":12,"pattern":"^[a-z0-9]+$","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRowsBody"}}}},"responses":{"200":{"description":"Records updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRowsResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found (workspace, module, table, or record)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"Payload too large (max 6 MB)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Records"]}}}}
```

## Get a single record from a table

> Retrieves a single record by its id. The record id is not a filterable field, so this is the only way to fetch a known record by id.

```json
{"openapi":"3.0.0","info":{"title":"Ninox Public API","version":"1.0.0"},"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"API Key","description":"Enter your Workspace API key. \n\n API keys can be generated and managed within the **Workspace Integration** settings in the Ninox app.","in":"header","name":"Authorization","type":"http"}},"schemas":{"RowResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"values":{"type":"object","additionalProperties":{"type":["boolean","null","number","string"]}}},"description":"The requested row"}},"required":["data"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}},"required":["error"]}}},"paths":{"/api/v1/workspace/{workspaceId}/modules/{moduleName}/tables/{tableName}/record/{recordId}":{"get":{"operationId":"RowsV1Controller_getRow","summary":"Get a single record from a table","description":"Retrieves a single record by its id. The record id is not a filterable field, so this is the only way to fetch a known record by id.","parameters":[{"name":"moduleName","required":true,"in":"path","description":"Module name","schema":{"minLength":1,"type":"string"}},{"name":"recordId","required":true,"in":"path","description":"Record ID","schema":{"maximum":9007199254740991,"exclusiveMinimum":true,"type":"integer","minimum":0}},{"name":"tableName","required":true,"in":"path","description":"Table name","schema":{"minLength":1,"type":"string"}},{"name":"workspaceId","required":true,"in":"path","description":"Workspace ID","schema":{"minLength":12,"maxLength":12,"pattern":"^[a-z0-9]+$","type":"string"}},{"name":"fields","required":false,"in":"query","description":"Comma-separated list of field names to include","schema":{"type":"string"}},{"name":"choiceStyle","required":false,"in":"query","not":{},"schema":{}},{"name":"dateStyle","required":false,"in":"query","not":{},"schema":{}},{"name":"style","required":false,"in":"query","not":{},"schema":{}}],"responses":{"200":{"description":"Record retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RowResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Table or record not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Records"]}}}}
```

## Get row changes since a timestamp

> Retrieves rows created, updated, or deleted since a given timestamp — for incremental sync. Only available for tables with history tracking enabled; returns 400 otherwise. Pass the response's \`meta.asOf\` as \`since\` on the next poll to avoid gaps or duplicates. When paging through one poll with \`limit\`/\`offset\`, also pass the first page's \`meta.asOf\` back as \`asOf\` on every subsequent page of that same poll.

```json
{"openapi":"3.0.0","info":{"title":"Ninox Public API","version":"1.0.0"},"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"API Key","description":"Enter your Workspace API key. \n\n API keys can be generated and managed within the **Workspace Integration** settings in the Ninox app.","in":"header","name":"Authorization","type":"http"}},"schemas":{"RowChangesResponse":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"changedAt":{"type":"string","description":"When the change happened (ISO 8601)"},"changeType":{"type":"string","enum":["create","delete","update"],"description":"Kind of change"},"id":{"type":"string","description":"Row id"}},"required":["changedAt","changeType","id"]},"description":"List of row changes"},"meta":{"type":"object","properties":{"asOf":{"type":"string","description":"Server timestamp to pass as `since` on the next poll"}}},"page_info":{"type":"object","properties":{"has_more":{"type":"boolean","description":"Whether more results are available"},"limit":{"type":"number","description":"Maximum results per page"},"offset":{"type":"number","description":"Current offset"}},"description":"Pagination information"}},"required":["data","meta","page_info"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}},"required":["error"]}}},"paths":{"/api/v1/workspace/{workspaceId}/modules/{moduleName}/tables/{tableName}/records/changes":{"get":{"operationId":"RowsV1Controller_getRowChanges","summary":"Get row changes since a timestamp","description":"Retrieves rows created, updated, or deleted since a given timestamp — for incremental sync. Only available for tables with history tracking enabled; returns 400 otherwise. Pass the response's `meta.asOf` as `since` on the next poll to avoid gaps or duplicates. When paging through one poll with `limit`/`offset`, also pass the first page's `meta.asOf` back as `asOf` on every subsequent page of that same poll.","parameters":[{"name":"moduleName","required":true,"in":"path","description":"Module name","schema":{"minLength":1,"type":"string"}},{"name":"tableName","required":true,"in":"path","description":"Table name","schema":{"minLength":1,"type":"string"}},{"name":"workspaceId","required":true,"in":"path","description":"Workspace ID","schema":{"minLength":12,"maxLength":12,"pattern":"^[a-z0-9]+$","type":"string"}},{"name":"asOf","required":false,"in":"query","description":"Pin the poll window to this timestamp (a prior response's `meta.asOf`) when fetching page 2+ of the same poll; omit on the first page","schema":{"format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","type":"string"}},{"name":"limit","required":false,"in":"query","description":"Maximum number of changes to return (1-100)","schema":{"minimum":1,"maximum":100,"type":"number"}},{"name":"offset","required":false,"in":"query","description":"Number of changes to skip (starts with 0)","schema":{"minimum":0,"type":"number"}},{"name":"sinceSeq","required":false,"in":"query","not":{},"schema":{}},{"name":"sinceSq","required":false,"in":"query","not":{},"schema":{}},{"name":"since","required":true,"in":"query","description":"Return changes at or after this ISO 8601 timestamp","schema":{"format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","type":"string"}}],"responses":{"200":{"description":"Row changes retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RowChangesResponse"}}}},"400":{"description":"Bad request (missing `since`, or table has history tracking disabled)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The API key lacks the records:read grant, or the table is not readable by the key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Table not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Records"]}}}}
```

## Import records from CSV file

> Import records from a CSV file into an existing table.\
> &#x20;       \
> The endpoint accepts multipart/form-data with:\
> \- A CSV file (max 50 MB)\
> \- Optional parsing and import options\
> \
> \*\*Auto-mapping\*\*: By default, CSV columns are automatically mapped to table fields by matching column header names to field names (case-insensitive).\
> \
> \*\*Import Modes\*\*:\
> \- \`append\`: Always create new records (default)\
> \- \`update\`: Only update existing records (requires key field mappings)\
> \- \`upsert\`: Update existing records or create new ones (requires key field mappings)\
> \
> \*\*Field Mappings\*\*: Optionally provide explicit mappings to control which CSV columns map to which table fields, and which fields are used as keys for update/upsert operations.\
> \
> \*\*Choice and multi fields\*\*: A cell for a \`choice\` field holds the option id or an option label, and it is stored as the option id. A cell that matches no option fails the whole import with a 400, and an empty cell is stored as empty. \`multi\` fields cannot be imported from a CSV cell, so a \`multi\` column with a value fails the import with a 400. Set \`multi\` values through the records endpoints. Records read back return the option id for both.\
> \
> The entire import is processed within a single transaction - if any record fails to import, all changes are rolled back.

```json
{"openapi":"3.0.0","info":{"title":"Ninox Public API","version":"1.0.0"},"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"API Key","description":"Enter your Workspace API key. \n\n API keys can be generated and managed within the **Workspace Integration** settings in the Ninox app.","in":"header","name":"Authorization","type":"http"}},"schemas":{"ImportCSVResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"rowsImported":{"type":"number","description":"Number of rows successfully imported"},"rowsSkipped":{"type":"number","description":"Number of rows skipped (for update mode when no match is found)"},"rowsUpdated":{"type":"number","description":"Number of existing rows updated"}}}},"required":["data"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}},"required":["error"]}}},"paths":{"/api/v1/workspace/{workspaceId}/modules/{moduleName}/tables/{tableName}/records/import/csv":{"post":{"operationId":"RowsV1Controller_importCSV","summary":"Import records from CSV file","description":"Import records from a CSV file into an existing table.\n        \nThe endpoint accepts multipart/form-data with:\n- A CSV file (max 50 MB)\n- Optional parsing and import options\n\n**Auto-mapping**: By default, CSV columns are automatically mapped to table fields by matching column header names to field names (case-insensitive).\n\n**Import Modes**:\n- `append`: Always create new records (default)\n- `update`: Only update existing records (requires key field mappings)\n- `upsert`: Update existing records or create new ones (requires key field mappings)\n\n**Field Mappings**: Optionally provide explicit mappings to control which CSV columns map to which table fields, and which fields are used as keys for update/upsert operations.\n\n**Choice and multi fields**: A cell for a `choice` field holds the option id or an option label, and it is stored as the option id. A cell that matches no option fails the whole import with a 400, and an empty cell is stored as empty. `multi` fields cannot be imported from a CSV cell, so a `multi` column with a value fails the import with a 400. Set `multi` values through the records endpoints. Records read back return the option id for both.\n\nThe entire import is processed within a single transaction - if any record fails to import, all changes are rolled back.","parameters":[{"name":"moduleName","required":true,"in":"path","description":"Module name","schema":{"minLength":1,"type":"string"}},{"name":"tableName","required":true,"in":"path","description":"Table name","schema":{"minLength":1,"type":"string"}},{"name":"workspaceId","required":true,"in":"path","description":"Workspace ID","schema":{"minLength":12,"maxLength":12,"pattern":"^[a-z0-9]+$","type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"CSV file to import (max 50 MB)"},"hasHeader":{"type":"boolean","default":true,"description":"Whether the CSV file has a header row"},"delimiter":{"type":"string","description":"CSV delimiter character (auto-detected if not specified)"},"quoteChar":{"type":"string","default":"\"","description":"CSV quote character"},"encoding":{"type":"string","enum":["utf8","ascii","latin1","iso-latin1","utf16","windows-cp1252"],"default":"utf8","description":"CSV file encoding"},"importMode":{"type":"string","enum":["append","update","upsert"],"default":"append","description":"Import mode: append (insert all), update (update existing only), upsert (insert or update)"},"numberFormat":{"type":"string","enum":["us","european"],"default":"us","description":"Number format for parsing numeric values"},"batchSize":{"type":"integer","minimum":10,"maximum":200,"default":100,"description":"Number of records to process per batch"},"mappings":{"type":"string","description":"JSON array of field mappings: [{\"csvColumnName\": \"Name\", \"fieldName\": \"name\", \"updatePolicy\": \"key\"}]"}}}}}},"responses":{"201":{"description":"CSV import completed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportCSVResponse"}}}},"400":{"description":"Bad request (invalid CSV, missing file, etc.)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Table not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"File too large (max 50 MB)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Records"]}}}}
```

## Upsert records in a table by a unique field

> Creates or updates records in a table, matched by a single explicit field (max. 6MB payload size). The match field must be marked \`unique: true\` on the table. Every record must include a value for the match field. An ambiguous match (more than one existing record with the same value) is rejected with a 409, never resolved silently.

```json
{"openapi":"3.0.0","info":{"title":"Ninox Public API","version":"1.0.0"},"security":[{"bearer":[]}],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"API Key","description":"Enter your Workspace API key. \n\n API keys can be generated and managed within the **Workspace Integration** settings in the Ninox app.","in":"header","name":"Authorization","type":"http"}},"schemas":{"UpsertRowsBody":{"type":"object","properties":{"matchField":{"type":"string","minLength":1,"description":"Name of the unique field used to match existing records"},"records":{"minItems":1,"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Array of records to upsert — each must include a value for matchField"}},"required":["matchField","records"],"additionalProperties":false},"UpsertRowsResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"createdIds":{"type":"array","items":{"type":"string"},"description":"Row ids created by this upsert"},"updatedIds":{"type":"array","items":{"type":"string"},"description":"Row ids matched and updated by this upsert"}}}},"required":["data"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string","description":"Error message"}}}},"required":["error"]}}},"paths":{"/api/v1/workspace/{workspaceId}/modules/{moduleName}/tables/{tableName}/records/upsert":{"post":{"operationId":"RowsV1Controller_upsertRows","summary":"Upsert records in a table by a unique field","description":"Creates or updates records in a table, matched by a single explicit field (max. 6MB payload size). The match field must be marked `unique: true` on the table. Every record must include a value for the match field. An ambiguous match (more than one existing record with the same value) is rejected with a 409, never resolved silently.","parameters":[{"name":"moduleName","required":true,"in":"path","description":"Module name","schema":{"minLength":1,"type":"string"}},{"name":"tableName","required":true,"in":"path","description":"Table name","schema":{"minLength":1,"type":"string"}},{"name":"workspaceId","required":true,"in":"path","description":"Workspace ID","schema":{"minLength":12,"maxLength":12,"pattern":"^[a-z0-9]+$","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertRowsBody"}}}},"responses":{"200":{"description":"Records upserted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertRowsResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found (workspace, module, table, or match field)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Ambiguous match — more than one record matches the given value","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"Payload too large (max 6 MB)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"tags":["Records"]}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.ninox.com/ninox-api/de/api-reference/api-endpoints/records.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
