PraxQL Overview
Vincent Depassier · August 29, 2026
PraxQL — Overview
PraxQL is Praxsuite's JSON-based declarative query language. It is what you use to read and write data through the API Gateway. Instead of configuring one REST endpoint for every data access pattern, you send a single structured JSON body that describes exactly what you want.
Why PraxQL exists
Praxsuite tables are created by users at runtime, not defined by a developer at deploy time. A workspace's shape changes whenever somebody adds a table or renames a column, and it differs from every other workspace.
That rules out the usual off-the-shelf options. GraphQL wants a schema fixed at deploy time. Auto-generated REST layers assume a database structure that is stable and safe to expose. OData cannot describe a schema that differs per tenant and changes while the service is running.
PraxQL was built for that situation: one endpoint, one request format, and a schema you discover at runtime rather than one you compile against.
The request structure
Every PraxQL call is a POST to one endpoint:
POST https://gateway.praxsuite.com/{workspaceId}/query
Authorization: Bearer sk_live_xxxxx
Content-Type: application/jsonThat short form is the one to use. The gateway also answers the longer /api/v1/gateway/{workspaceId}/query path, which is what you will see in older examples — same endpoint, same body.
The request body always has two parts:
{
"refs": {
"Alias": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"query": { ... }
}Or, for write operations:
{
"refs": {
"Alias": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"mutation": { ... }
}query and mutation are mutually exclusive — send one or the other.
refs — the table reference dictionary
The refs object maps your chosen aliases to table GUIDs. It exists because table names are not unique: a workspace can have several tables called "Customers" at different points in time, so a name is not enough to identify one.
{
"refs": {
"Customers": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"Orders": "b2c3d4e5-f6a7-8901-bcde-f12345678901"
}
}Rules:
Keys are any string you choose — they become your alias in the
query/mutationbody.Values are the table GUIDs. Get them from the Playground's schema panel or from
GET /schema.Every GUID must belong to the authenticated workspace. A reference to another workspace's table is refused.
Up to 20 refs per request.
refs is also the complete list of what a request is allowed to touch, which is what lets the gateway check a request against your permissions before doing any work.
query vs mutation
Key | Operations | When to use |
| Read data | Fetching records, filtering, aggregating, joining |
| Write data | Inserting rows, updating fields, deleting records |
Optional: includeTotalCount
By default the response carries no total count of all matching rows — it is skipped because it costs a second pass over the data. To ask for it:
{
"refs": { ... },
"query": { ... },
"includeTotalCount": true
}Only ask for it when you are drawing pagination controls that show a total; a plain "next page" button does not need it.
The response shape
{
"data": [
{ "Name": "Acme Corp", "Email": "hello@acme.com" },
{ "Name": "Beta Inc", "Email": "info@beta.io" }
],
"meta": {
"count": 2,
"limit": 50,
"offset": 0,
"total": null,
"durationMs": 4
}
}Field | Meaning |
| Array of row objects. Keys are logical column names. |
| Number of rows in this response |
| The effective limit applied |
| The offset applied |
| Total matching rows — only set when |
| Time spent executing, excluding authentication |
meta.limit is worth reading rather than assuming: the effective limit can be lower than what you asked for.
Error responses
Every error returns a structured body:
{
"error": {
"code": "INVALID_QUERY",
"message": "Column 'Emaill' not found in table 'Customers'.",
"details": ["Available columns: Name, Email, Phone, Status"]
}
}HTTP | Code | Cause |
|
| Malformed query, unknown column, invalid operator |
|
| Unknown table GUID, a GUID from another workspace, empty refs |
|
| Invalid or missing API key |
|
| Table or column not in scope, aggregation not allowed |
|
| Key revoked or expired, or the principal is suspended |
|
| Query exceeded the 30-second timeout |
|
| Key exceeded its request quota |
|
| Internal error, details recorded server-side |
SCOPE_VIOLATION is worth understanding: a table your key has no permission for answers 403, not 404, and the message does not confirm whether the table exists. That is deliberate — a 404 would let a caller map your workspace by probing.
What happens to a request
Every call is authenticated, checked against the permissions of the credential that sent it, translated from the logical names you used into whatever the storage layer needs, executed, and then filtered again on the way out so that masking rules apply to the results.
The last part matters for how you design queries: permissions are enforced on the way in and on the way out. A column you may filter on but not read still participates in the where clause while never appearing in the response.
Every request is also recorded in the audit log — including the ones that failed.