` |
| Lifetime | The token stopped working 266 seconds after being pasted into the test. It was fetched shortly before that, so this fits the documented 5 minutes without confirming it |
| Expired, or out of scope | Both return 401, `Invalid Authorization.`, so they can't be told apart |
| A malformed token, or a user token with this header | 400, `Invalid Authorization` |
| A temporary token with `QB-USER-TOKEN` | 401, `User token is invalid` |
| Requesting another token with one | 400, `Domain not allowed` |
| Request | Table-scoped token | App-scoped token |
|---|---|---|
| Records in its table: query, write | **200, including writes** | 401 |
| Fields and reports of its table, running a report or formula | 200 | 401 |
| Another table: query, write, fields | 401 | 401 |
| `GET /tables` and `GET /apps/{id}` | **401** | 200 |
| **A formula that reads another table** | **200, and returns that table's data** | 401 |
Tokens are scoped narrowly, and an app-scoped token doesn't reach its tables' contents. **Table scope
doesn't contain a formula**: `GetRecord()` read a table the token couldn't otherwise reach. Uploads,
downloads, and file deletes weren't tested with a temporary token. A token from a user with fewer
permissions was only tested on a delete: see [Code pages and dashboards](https://matthew-williams-solutions.github.io/pyquickbase/api-notes/#code-pages-and-dashboards).
### Code pages and dashboards
Tested on 2026-11-03 in a sandbox app, in a logged-in browser, using a code page that runs Python in a
sandboxed frame. Dashboard settings were checked by hand, in the app's own screens.
| Behavior | Result |
|---|---|
| A code page's security policy | `default-src * 'unsafe-eval' 'unsafe-inline' data: filesystem: about: blob: ws: wss; worker-src 'none';`, so a page can't start a worker. Almost everything else is allowed, so any tighter limit has to come from the page itself |
| A code page in a dashboard's Web page widget | **Served from `realm.ui.quickbase.com`, and `GET /auth/temporary` fails there with no CORS header.** Reloading the page from the realm's own address, inside the widget, works |
| Saving a code page that contains `QB_USER_TOKEN="..."` | A warning that storing secrets in a browser page is insecure, even for a placeholder. It doesn't prevent saving, and moving the value into a variable didn't clear it |
| The dashboard rich text editor | Removes inline styles and collapses line breaks in ``. `
` and ` ` work |
| KPI widgets | Built from a gauge chart report. Tile colors come from the report's Low, Medium, and High legend colors |
| KPI widget settings | **Show target** hides the `TARGET` line, and **Custom title** replaces the report's name, so neither needs a change to the report |
| A gauge report saved with a goal of 0 | Still displays, and its settings page marks the goal "Must be greater than start" |
| Chart widget settings | Only **Report** and **Custom title**. Everything else is set on the report |
| A horizontal bar chart in a narrow widget | **Its category labels disappear, while the bars and axis remain.** There's no setting for this in the widget |
| Table report **Options** | **Show edit icon** and **Show view icon** remove the row icons, and **Hide row numbers** works on a table report in a dashboard, though the help only describes it for grid reports |
| A narrow browser window | The dashboard doesn't rearrange its widgets. It scrolls sideways, so a Web page widget keeps its width |
| Dashboard reports after a write through the API | **They don't update until the page is reloaded** |
| A delete with a table-scoped temporary token, from a role without delete rights | 403, `Insufficient permissions: Not enough rights to execute this request.` |
| The same delete with a filter that matches nothing, `{3.LT.0}` | Also 403, so it shows whether a role can delete before anything changes |
### Restricted users
Tested on 2026-11-02 against a temporary table in a sandbox app, with a second user's token. Its role
could only view that table, had no access to the app's other tables, and had access `None` on one text
field and one file field. See also [roles with write access](https://matthew-williams-solutions.github.io/pyquickbase/api-notes/#roles-with-write-access) and
[record-level rules](https://matthew-williams-solutions.github.io/pyquickbase/api-notes/#record-level-rules).
| Behavior | Result |
|---|---|
| A token's permissions | Its user's, through their role |
| `GET /fields` | **A hidden field is left out**, exactly as if it didn't exist, so a deleted field and a hidden one can't be told apart |
| `GET /fields/{id}` for a hidden field | 500, `QuickBase is experiencing technical difficulties` |
| `GET /tables` | Lists only tables the role can use |
| A table created through the API | The role gets no access to it, so `GET /tables` leaves it out until its permissions are set in the UI. Live API, 2026-11-06 |
| `GET /apps/{id}` | 403, for this role |
| `select` with a hidden field and a visible one | **200, and the hidden field is silently left out** |
| `select` with only hidden fields | 400, `Report contains a restricted field` |
| `where` or `sortBy` on a hidden field | 403, whether or not anything matches |
| Running a table report with a hidden column | **200, and the column is silently left out** |
| A file field's value, read with view access | Includes `creator`, with **the uploader's email address** |
| Downloading from a visible file field | 200 |
| Downloading from a hidden file field | 404, `No such field` |
| A formula using a hidden field, directly or through `GetRecords()` | 403, including when `GetRecords()` only filters on it. A visible field works |
| Insert, update, delete, upload, deleting a file version, creating a field | 403, `Insufficient permissions`, and nothing changed |
| A table the role has no access to | `GET /tables` leaves it out. Querying it, `GET /tables/{id}`, `GET /fields`, and a formula with it as `from` return 403. Tested on 2026-11-04 |
| **A formula reading a table the role has no access to** | **Empty, with no error.** `GetRecord()`, `GetRecords()`, and `Size()` return 200 with `""` from a table the role can view, while an admin's token gets the real value. A saved formula field shows each user their own result. Tested on 2026-11-04 with a role that had no access to the table or its fields |
### Roles with write access
Tested on 2026-11-04 in a real app, with a second user's token. Its role had custom field access on some
tables, and no delete rights. See also [record-level rules](https://matthew-williams-solutions.github.io/pyquickbase/api-notes/#record-level-rules).
| Behavior | Result |
|---|---|
| Add off, and Modify on for the table | 207, with the row error `You don't have permission to add records to this table.` |
| Add and Modify both off for the table | **403, `Insufficient permissions`**, for inserts and updates, so the same refusal takes a different form |
| Modify off, with an upsert that matches an existing record | Row error, `You don't have permission to update records in this table.` An upsert that creates a record works |
| Inserting a duplicate into a unique field | Row error, `Adding non-unique value for field with ID "8".`, and no new record, with only add rights |
| A field set to View in the role | Readable and usable in `where`. Writing it gives the row error `Insufficient permissions to update data in a restricted field.` |
| **A row with an allowed field and a view-only field** | **The whole row is refused**, and the allowed field doesn't change |
| **A field created after a role's Custom Access was set** | **The role gets full access to it.** It could write the new field straight away, until it was set to View. Check a role after adding any field |
| `where` on lookup and foreign key fields | Works |
| Upsert with add and modify rights, including repeating it | Works |
| `DELETE /records` with no delete rights, with `{3.LT.0}` | 403 on every table |
| `POST /fields` and `POST /tables` | 403 |
| `GET /apps/{id}` and `GET /apps/{id}/roles` | **403**. `GET /tables` and `GET /fields` work |
`GET /apps/{id}/roles` with an administrator's token returns `id`, `name`, and `access`, including roles
you made. We didn't find any endpoint for reading or changing a role's permissions:
`/apps/{id}/roles/{id}` and `/apps/{id}/roles/{id}/permissions` return 404.
### Record-level rules
Tested on 2026-11-04 against 6,000 records split evenly across three categories, with a second user's
token. The role's View rule was `Category` is not `Gamma`, and its Modify rule was `Category` is
`Alpha`. It had Add but not Delete, and a hidden numeric field.
| Behavior | Result |
|---|---|
| Queries, counts, and reports | Only show records the View rule allows, and `totalRecords` counts only those, so nothing reveals how many others exist |
| `where` matching a hidden record, by ID or value | 200 with no records, not an error |
| `GetRecords()` and `GetRecord()` in a formula | Follow the rule. A count is of allowed records, and a hidden record reads as `""` |
| A formula with a hidden record as `rid` | **403, while a record that doesn't exist returns 404**, so this shows whether an ID exists |
| Updating a record the Modify rule doesn't allow, visible or hidden | Row error, `You don't have permission to update records in this table.` |
| Updating an ID that doesn't exist | Row error, `Can not find record by ID`, **which also shows whether an ID exists** |
| A request mixing allowed and refused records | 207. The allowed rows are written, and the others get row errors |
| **Changing an allowed record so the rules no longer match** | **Allowed, including moving it to where the role can't see it.** The rules check the record before the change, not after |
| **Inserting a record the role won't be able to see** | **Allowed.** The new ID comes back, but `fieldsToReturn` values don't, and the role can't read it afterward |
| Deleting, with no delete rights | 403, including a record the role just added |
| Writing to a hidden field | Row error, `Insufficient permissions to update data in a restricted field.` |
**Reports using a hidden field.** A report that sorts or filters on a field the role can't see isn't
available to that role, and nothing about the hidden field is revealed:
| Behavior | Result |
|---|---|
| `GET /reports` | **Leaves the report out**, so the SDK raises `UnknownReportError`, by name or ID |
| `POST /reports/{id}/run` | **400, `Invalid request: Internal error`.** A report ID that doesn't exist returns 404, so this shows that the report exists |
| A report that only shows the hidden field as a column | Listed, and runs without that column |
| `GET /reports/{id}` | 403 for every report we tried with this role, even ones it can run |
### Custom data rules
Rules are written in the table's Advanced settings, and the API can't create them. Tested on 2026-11-04 in a
real app. The help says they also cover imports, grid edit, and connected table refreshes, which weren't tested.
| Behavior | Result |
|---|---|
| A rule refusing an update or insert through the API | 207, with the row error `Table rule validation: ` |
| **An administrator** | **Is bound by the rule**, through both the API and a form |
| A pipeline's write | Is bound by the rule. The run fails with `Custom data rule failed`, Error Code 88 |
| Deleting a record | The help only mentions adding and editing, and this wasn't tested |
| **`IsNull()` on an empty numeric field with `blankIsZero: true`** | **False, because the field counts as 0.** A rule using it refused every record until `blankIsZero` was set to false, which the API accepts |
### Pipelines
Tested on 2026-11-04 in a real app, with the Quickbase channel's **Record Updated** trigger and **Update Record**
action. Pipelines can't be created or read through this API.
| Behavior | Result |
|---|---|
| An update through the API | Fires the trigger, and the action had run within 4 seconds |
| A field the action reads | Must be listed in the trigger's "Fields for subsequent steps" |
| The trigger's filter | `Status is Open` didn't fire on a change from Open to Draft, and did on Draft to Open |
| The record changing before the action runs | The action works on the record as it is then, so it can hit a custom data rule |
### Relationships
Tested on 2026-11-03 and 2026-11-04 in a real app, using `POST /tables/{child}/relationship/{id}`.
| Behavior | Result |
|---|---|
| `lookupFieldIds` | **The parent's field IDs, and it adds to the existing lookups.** Sending an existing lookup's ID, which is on the child, looked up whichever parent field has that number, and created a field nobody asked for |
| A new lookup's label | ` - `, such as `Buyer - Name` on a parent whose record name is `Client`, and it can be renamed |
| A second `POST /tables/{child}/relationship` to the same parent | **Makes another relationship**, even with the same foreign key label |
| `accumulationType` values | The API lists `AVG`, `SUM`, `MAX`, `MIN`, `STD-DEV`, `COUNT`, `COMBINED-TEXT`, `COMBINED-USER`, and `DISTINCT-COUNT`, in capitals |
| Summaries that worked | `SUM` of numeric and currency, `AVG` and `STD-DEV` of numeric, `MAX` and `MIN` of numeric and date, `DISTINCT-COUNT` of numeric and text, `COMBINED-TEXT` of text, and `COMBINED-USER` of user. Each kept the child's type, except that `AVG`, `STD-DEV`, and `DISTINCT-COUNT` are numeric, and the combined ones are `multitext` and `multiuser` |
| Summaries refused with 400 | `SUM` of checkbox, `MAX` of text, and `COUNT` of numeric, text, checkbox, and the record ID |
| **`COUNT` takes no field** | With no `summaryFid`, or `summaryFid: 0`, it counts the child records, as a `numeric` summary with `summaryTargetFieldId: 0`. With any field, including the record ID, it's 400, `That form of summarization/aggregation cannot be applied to this field`. Lowercase `count` is 400. Live API, 2026-11-05 |
| **A `COUNT` with no child records** | **0, not empty**, unlike the other summaries. Live API, 2026-11-05 |
| A lookup's properties | `lookupReferenceFieldId` is the foreign key on the child, and `lookupTargetFieldId` is the field on the parent |
| `summaryFields`, with `summaryFid`, `label`, and `accumulationType: "SUM"` | Creates the field on the parent. Works on a formula field of the child |
| A summary's properties | `summaryReferenceFieldId`, `summaryTargetFieldId`, and `summaryFunction` |
| **A summary with no child records** | **Empty, not 0**, except a `COUNT`. A formula using it is also empty unless it uses `Nz()`, which matters for something like a balance |
| Fields added to the parent | A `dblink` such as `Task records`, and a `url` formula such as `Add Task`, containing `a=API_GenAddRecordForm` |
| `DELETE /tables/{child}/relationship/{id}` | 200, `{"relationshipId": 7}`. **Deletes the lookups on the child, and the summaries and the link and add-record fields on the parent. The foreign key field stays.** A second delete is 404, `Parent table not found. Delete the field 'Parent' to remove this relationship.` Live API, 2026-11-06 |
| `GET /tables/{parent}/relationships` | **Empty for the parent.** Only the child lists a relationship. Live API, 2026-11-06 |
| `where` on a `summaryFields` item | **Filters the child records the summary combines**, in a create or an update of the relationship, in Quickbase's query language. With child records of 10 and 7 matching and 5 not, a filtered `SUM` read 17 and an unfiltered one 22, and a filtered `COUNT` read 2. Live API, 2026-11-06 |
| A summary's filter, read back | `properties.summaryQuery` from `GET /fields` and `GET /fields/{id}`, exactly as sent. **It's left out for a summary with no filter.** The relationship's response doesn't include it. Live API, 2026-11-06 |
| **A summary filter Quickbase can't use** | **Saved with 200 and no error**, both a malformed one (`{nonsense`) and one on a field that doesn't exist (`{999.EX.true}`). The summary then reads as empty. Live API, 2026-11-06 |
| **Summaries and the reader's role** | **They only total the child records the reader can see.** With 4 child records and a custom rule letting a restricted role see 2, an admin read a count of 4 and a total of 40, while the restricted user read 2 and 20. With View set to None on the child table, the restricted user read both as empty, even the `COUNT`. Live API, 2026-11-06 |
### Field types
Tested on 2026-11-02 against temporary tables in a sandbox app. These decide the types in generated
classes, and an unlisted type becomes `Any` and read-only.
`POST /fields` accepts these types: `text`, `text-multiple-choice`, `text-multi-line`, `rich-text`,
`numeric`, `currency`, `rating`, `percent`, `multitext`, `email`, `url`, `duration`, `date`,
`datetime`, `timestamp`, `timeofday`, `checkbox`, `user`, `multiuser`, `address`, `phone`, and `file`.
A `datetime` field reports its type as `timestamp`.
| Field type | A query returns | When empty |
|---|---|---|
| `recordid` | Integer | Never empty |
| `text`, `text-multiple-choice`, `text-multi-line`, `rich-text`, `email`, `url`, `phone` | Text | **`""`, not null** |
| `numeric`, `currency`, `rating`, `percent` | Number | null |
| `duration` | Milliseconds | null |
| `date` | `2026-03-04` | `""` |
| `timestamp` | `2026-03-04T05:06:07Z` | `""` |
| `timeofday` | `13:30:00` | `""` |
| `checkbox` | Boolean | `false` |
| `multitext` | A list of text | `[]` |
| `user` | `{"email", "id", "name"}` | null |
| `multiuser` | A list of users | `[]` |
| `file` | `{"url", "versions"}` | `{"url": "", "versions": []}` |
| `address` | Display text, such as `1 Main St, Springfield 12345` | `""` |
| `dblink` | `""`, including when the relationship has child records | `""` |
| `vCardButton`, `ICalendarButton` | A relative link, such as `?a=VCard&fid=14&rid=b`, on every record, even with no data to show | The link, or `""` as a lookup with no parent |
| `predecessor` | Record IDs as text, such as `1` or `1;2` | `""` |
| Behavior | Result |
|---|---|
| Formula fields | Return the same shapes as their type. A formula of `Null` matched its type on text, numeric, and checkbox fields |
| Lookup fields | Return exactly what the parent field returns, filled and empty, for every type `POST /fields` accepts except `file`, and for numeric, text, and checkbox formulas. With no parent record, they read as that type's empty value. Tested on 2026-11-04 |
| A lookup's reported type | The parent's type, except that a lookup of `text-multiple-choice` **reports `text`** |
| A lookup of a `file`, `dblink`, or `predecessor` field | 400, `The following field ids can not be lookup fields` |
| A lookup of a type only the UI can create | `vCardButton`, `ICalendarButton`, and a `multiuser` formula return what the parent returns, and report the parent's type. Tested on 2026-11-04 |
| Types only the UI can create | vCard, iCalendar, and Predecessor, which report as `vCardButton`, `ICalendarButton`, and `predecessor`. A Report link reports as `dblink`, and a relationship also creates one |
| Writing to `vCardButton`, `ICalendarButton`, or `dblink` | Row error, `Can not change the value of the read-only field` |
| Writing to `predecessor` | Works. It takes a record ID, as text or a number, or several in a list or a separated string, stored as written: `1,2` reads as `1, 2`. A missing ID or other text gives `Invalid entry in field`, and a record linking to itself gives a row error about links back to the first record. `""` clears it. `where` with `EX` matched a record holding that ID |
| Writable fields | Built-in fields 1 to 5 aren't, nor are formula, lookup, and summary fields, or `address` |
| An `address` field | Is read-only, with text subfields for its parts, which can be written. Creating one also creates its subfields, and sets `Country` to a default |
| `dblink` | Created by a relationship, on the parent table |
| Labels | **Can contain almost anything, including `"""`, line breaks, and code**, as can table names |
| Label uniqueness | Exact duplicates are refused, ignoring case, but `Due Date` and `Due-Date` can both exist |
| **Spaces in a label** | **Kept exactly.** `" Spaced "`, `"Title "`, and `"Two Spaces"` are stored as sent, by creating and renaming, and `Title`, `" Title "`, and `"Title "` can all exist on one table. Live API, 2026-11-06 |
| Table names | `POST /tables` and renaming with `POST /tables/{id}` refuse a name already in use with 400, `Table name exists`, ignoring case and surrounding spaces |
| Changing a table with `POST /tables/{id}` | Needs `appId`, or it's 400, `Required query 'appId' not found`. An empty body is 200 and changes nothing. Live API, 2026-11-06 |
| Table names in the UI | Creating a table with a name already in the app, or renaming a table to one, is refused too. Tried by hand with the same name on 2026-11-04. Case and spacing variations weren't tried in the UI |
| Writing a timestamp | `2027-01-02T03:04:05+00:00` and three fractional digits are accepted. **Python's default of six, or one, is a row error**, `Incompatible value`. It reads back to the second |
| `properties` | An unknown key is refused with 400, `Extraneous key`. `fieldHelp` isn't a property, and neither are `choices` on a type without them |
| Settings outside `properties` | `POST /fields` and `POST /fields/{id}` take `fieldHelp`, `bold`, `noWrap`, `findEnabled`, and `appearsByDefault` beside `label`, and `GET /fields` returns them. `addToForms` is accepted on create and never returned. **`audited: true` was accepted and read back as false**, in an app without audit logging set up. `fieldHelp` inside `properties` is 400, `Extraneous key 'properties.fieldHelp' is not permitted`. Live API, 2026-11-05 |
| A field's response | Also lists `permissions`, each role's name, ID, and access to the field. `GET /fields` leaves them out |
| **Writing a `duration`** | **Milliseconds**, as it reads: `90000` reads back as `90000`, and `3600000` is an hour. Negative and 0 work. **A fraction is dropped with no error**, so `1.5` reads back as `1`. Text is parsed too: `"90000"`, `"1:30:00"`, and `"2 hours"` became `90000`, `5400000`, and `7200000`. Live API, 2026-11-05 |
| `POST /fields/{id}` with `required: true` | 400, `field contains null values`, if any record has it empty, which includes a new field on a table with records |
| `POST /fields/{id}` with `unique: true` | 400, `field contains non-unique values`, if it holds duplicates. A new, empty field on a table with records is allowed |
| A `phone` value | Reformatted: `5551234567` reads back as `(555) 123-4567` |
| A built-in field in `DELETE /fields` | Refused in `errors`, and the others in the list are deleted |
| Field IDs | Assigned in order, and address subfields use up IDs |
| `GET /tables/{id}` | Returned an error without its `appId`, so use `GET /tables?appId=` |
### Apps, field usage, and events
Tested on 2026-11-08 against the Integration Tests app and temporary apps the test made and deleted.
| Behavior | Result |
|---|---|
| `POST /apps` with `assignToken: true` | 200, and the same token can read the new app at once |
| `POST /apps/{id}/copy` | 200 with the copy's details and `ancestorId`. `keepData: true` copies the records, and without it the tables are empty |
| `DELETE /apps/{id}` with `{"name": ...}` | 200, `{"deletedAppId": ...}` |
| Listing apps | No endpoint, so an app left behind can only be found in the interface |
| `GET /fields/usage` on a table of 10 fields | **Every field, built-in ones included, in one response**, as `{"field": {"id", "name", "type"}, "usage": {kind: {"count": n}}}`. `type` is lower case, such as `text` |
| Kinds of use | `actions`, `appHomePages`, `customEmails`, `dashboards`, `defaultReports`, `documentTemplates`, `exactForms`, `fields`, `forms`, `notifications`, `personalReports`, `pipelines`, `relationships`, `reminders`, `reports`, `roles`, `tableImports`, `tableRules`, `webhooks`. `customEmails` and `documentTemplates` aren't in the published spec |
| A new text field's usage | `reports: 2` and `defaultReports: 1` at once, and `forms: 1` a few seconds later, so **counts lag a change by a few seconds** |
| `GET /fields/usage` with `top` and `skip` | Pages as expected, but **a `skip` past the last field returns 500** |
| `GET /fields/usage/{id}` | A list with one entry |
| `GET /fields/usage` and `GET /apps/{id}/events` with a restricted user's token | **403**, `Insufficient permissions`, so both need an app administrator |
| `GET /apps/{id}` and `GET /tables/{id}/relationships` with a token whose role lacks "Edit app structure and permissions" | **403**, `Insufficient permissions`, on an app the user can open. `GET /tables`, `GET /fields`, and `GET /reports` still return 200 for the tables the role can see. Tested on 2026-11-08 |
| `GET /apps/{id}/events` on an app with none | 200, `[]` |
### Values written as text
Tested on 2026-11-08 on a temporary table.
| Value | Result |
|---|---|
| A `user` field set to an email address, in any case | Sets that user |
| A `user` field set to a user ID, or `{"id": email}` | Sets that user |
| **A `user` field set to `{"email": email}`** | **200, and the field is unchanged** |
| A `multiuser` field set to a list of emails, or one email as text | Sets those users |
| A `percent` field set to `"0.45"` | Reads as `0.45` |
| **A `percent` field set to `"1,234.5"` or `"45%"`** | **200, and the field is emptied** |
### Interface links
These are the current Quickbase interface's addresses, which the SDK's `*_url()` methods build. The interface isn't part of the API, so Quickbase
may change them without notice.
| Page | Path, after `https://` | Source |
|---|---|---|
| App home | `/nav/app//action/appoverview` | Browser, 2026-11-05 |
| App settings | `/nav/app//action/appsettingshome` | Browser, 2026-11-05 |
| Table home | `/nav/app//table//action/td` | Browser, 2026-11-05 |
| Report | `/nav/app//table//action/q?qid=` | Browser, 2026-11-05 |
| Show a record | `/nav/app//table//action/dr?rid=` | Browser, 2026-11-05 |
| Edit a record | `/nav/app//table//action/er?rid=` | Browser, 2026-11-05 |
| Edit a field's settings | `/nav/app//table//action/mf?fid=` | Browser, 2026-11-05 |
| Edit a report | `/nav/app//table//action/viewBuild?qid=` | Browser, 2026-11-05 |
| Add a record | `/nav/app//table//action/nwr` | Browser, 2026-11-05 |
| A table's fields | `/nav/app//table//action/listfields` | Browser, 2026-11-05 |
| A table's relationships | `/nav/app//table//action/Relationships` | Browser, 2026-11-05 |
| A table's settings | `/nav/app//table//action/KeyProps` | Browser, 2026-11-05 |
| Edit a form | `/nav/app//table/