Skip to main content
API Reference

Generated (data-plane) API reference

Complete reference for every generated tenant API — routes, query parameters, filter operators, request/response bodies, type mapping, authentication, error catalogue, and rate limiting.

Every RestWard tenant API is a generated REST API that exposes your database tables as RESTful resources. This page documents the exact shape, parameters, operators, authentication, errors and rate limits that every generated API supports, independent of your schema.

What changed since issue #165 (2026-09-28) See the discrepancy list at the bottom of this page.


Base URL & schema

Aspect Value
Base path /api/
Schema segment The database schema name: PostgreSQL public, SQL Server dbo, MySQL the database name, Oracle the connecting user (upper-cased), Db2 the current user's schema
Table segment The table name (case-sensitive as defined in the database)

Example: GET /api/public/artist lists rows from the artist table in PostgreSQL's public schema.


Routes per table

Every table is exposed with the following standard routes.

Method Route Operation Description
GET /api/{schema}/{table} List List rows with filtering, sorting, pagination and field projection
GET /api/{schema}/{table}/{id} Get by ID Retrieve one row by its primary key
POST /api/{schema}/{table} Create Insert a new row
PUT /api/{schema}/{table}/{id} Update Full update: replace the supplied columns; unknown, key and generated columns are silently ignored
PATCH /api/{schema}/{table}/{id} Partial update Strict partial update: update exactly the supplied columns; empty body, unknown column, primary-key column or generated column is a 400
DELETE /api/{schema}/{table}/{id} Delete Delete one row by its primary key

Composite primary keys

Tables with a multi-column primary key use a comma-separated list of values in the {id} route segment, one value per PK column in the order defined by the table's primary key columns.

Example: A table order_items with PK (order_id, line_number):

  • Route: GET /api/public/order_items/42,3
  • Returns the row where order_id = 42 AND line_number = 3

If the number of comma-separated segments does not match the number of PK columns, the request returns 400 with a clear message naming the table and the expected number of values.

Views

Views are not exposed. Every schema reflector returns base tables only, filtering by table_type = 'BASE TABLE'. Only base tables become endpoints.


Query parameters

All list (GET /api/{schema}/{table}) and get-by-ID (GET /api/{schema}/{table}/{id}) endpoints accept the following query parameters.

Parameter Type Default Description
limit integer 100 Number of rows to return; clamped to 1–1000
offset integer 0 Number of rows to skip; negative values count as 0
sort string Primary key ascending Comma-separated column names; prefix with - for descending (e.g., sort=artist_id,-title)
fields string All columns Comma-separated column names to return (projection); unknown or empty is a 400
count boolean false true includes the total number of matching rows in the response body and the X-Total-Count header

Filter parameters

Filters are specified as query parameters using one of two syntaxes:

  1. Simple equality: column=value (implicit eq operator)
  2. Explicit operator: column[op]=value where op is one of the supported operators

Supported filter operators (8):

Operator Syntax SQL equivalent Notes
eq column[eq]=value or column=value column = value Equality
neq column[neq]=value column <> value Not equal
gt column[gt]=value column > value Greater than
gte column[gte]=value column >= value Greater than or equal
lt column[lt]=value column < value Less than
lte column[lte]=value column <= value Less than or equal
like column[like]=value column LIKE value SQL pattern: % matches any text, _ matches one character
in column[in]=value column IN (value1,value2,...) Comma-separated list of values

Combining filters: Multiple filter parameters are combined with AND logic.

Unknown columns/operators: An unknown filter column or operator is not silently dropped — it returns a 400 error naming the parameter and the problem (#151 resolved).

Examples:

  • GET /api/public/artist?name=AC/DC — equality filter
  • GET /api/public/album?artist_id[gt]=5&artist_id[lte]=10 — range filter
  • GET /api/public/track?title[like]=%Rock% — pattern match
  • GET /api/public/invoice?status[in]=paid,pending — IN filter

Request & response bodies

Response: List rows

Without count=true:

JSON
[
  { "column1": "value1", "column2": 123 },
  { "column1": "value2", "column2": 456 }
]

With count=true:

JSON
{
  "total": 42,
  "items": [
    { "column1": "value1", "column2": 123 },
    { "column1": "value2", "column2": 456 }
  ]
}

Headers with count=true:

  • X-Total-Count: 42 — the total number of matching rows

Response: Get by ID

JSON
{ "column1": "value1", "column2": 123 }

Returns 404 if no row matches the primary key.

Request: Create

JSON
{ "column1": "value1", "column2": 123 }
  • Required: Columns that are NOT NULL and not auto-increment/generated
  • Ignored: Server-generated columns (identity keys, generated values), unknown columns
  • Response: 201 Created with { "id": <value> } where id is the primary key value(s)

Request: Update (PUT)

JSON
{ "column1": "new_value1", "column2": 456 }
  • Behavior: Updates the supplied columns; unknown, primary-key and generated columns are silently ignored
  • Empty body: Returns 400 if the body contains no updatable columns
  • Response: 204 No Content on success, 404 if no row matches the primary key

Request: Partial update (PATCH)

JSON
{ "column1": "new_value1" }
  • Behavior: Strict partial update — updates exactly the supplied columns
  • Validation: Empty body, unknown column, primary-key column or generated column returns 400 naming the column
  • Response: 204 No Content on success, 404 if no row matches the primary key

Request: Delete

No request body. Returns 204 No Content on success, 404 if no row matches the primary key.


Type mapping per provider

Column types in responses and request bodies are mapped from the database's native types to JSON types as follows. All providers use the common mapping as the baseline; provider-specific overrides are noted.

Common type mapping (PostgreSQL, SQL Server, MySQL, Oracle, Db2)

Native type(s) JSON type OpenAPI type Notes
integer, int, int4, serial, smallint, int2, tinyint number integer
bigint, int8, bigserial number integer
real, float4, float number number
double precision, float8, double, decimal, numeric, money number number
boolean, bool, bit boolean boolean
timestamp, timestamptz, timestamp without time zone, timestamp with time zone, datetime, datetime2, date string string ISO 8601 format (e.g., 2024-01-15T10:30:00Z)
All other types string string Includes text, varchar, char, nvarchar, nchar, clob, blob, etc.

Null handling

  • In responses: null database values are serialized as JSON null
  • In requests: null JSON values are passed as NULL to the database; omitting a column is different from sending null
  • NOT NULL columns: Omitting a NOT NULL column on POST returns 422; sending null for a NOT NULL column also returns 422

Timestamp handling

  • Format: ISO 8601 strings (e.g., 2024-01-15T10:30:00Z, 2024-01-15T10:30:00+01:00)
  • Time zone: Timestamps without time zone information are treated as UTC; timestamps with time zone are preserved
  • Precision: Sub-second precision is preserved where the database supports it

Authentication

Every generated API supports the authentication methods enabled for its organization. The runtime checks credentials in the following order and uses the first valid one.

Supported authentication methods

Method Header Description
API Key X-API-Key: <key> Simple key-based authentication; key is hashed with SHA-256 for storage and lookup
JWT Bearer Authorization: Bearer <token> JSON Web Token; the token's org_id claim must match the API's organization
mTLS Client certificate Mutual TLS; the client certificate's thumbprint is used as the credential

Authentication flow

  1. The route's required authentication type is determined by:
    • The endpoint configuration from the dashboard (#149) — if set, only that method is accepted
    • Otherwise, the route's static metadata (legacy default: API Key)
  2. If the route is Public, no authentication is required
  3. For authenticated routes, the middleware tries each enabled method in order (API Key, JWT, mTLS) and uses the first valid credential
  4. If no valid credential is found, the request returns 401 Unauthorized
  5. If the credential is valid but the caller lacks permission for the operation, the request returns 403 Forbidden

Public endpoints

Endpoints can be configured as Public in the dashboard, which means:

  • No authentication is required
  • No 401 or 403 responses are possible
  • Rate limiting still applies (endpoint limit and platform limits)
  • No API key quota is applied (only the endpoint's own per-caller limit)

Error catalogue

Every error response has a JSON body with at least an error field. Additional fields may be present depending on the error type.

Error response body

JSON
{
  "error": "Human-readable error message"
}

Additional fields that may be present:

  • "column": "column_name" — the column that caused the error
  • "constraint": "constraint_name" — the database constraint that was violated
  • "parameter": "param_name" — the query parameter that caused the error

HTTP status codes

Code Title When returned Body shape
400 Bad Request Invalid query parameters, malformed JSON, unknown columns/operators, duplicate columns in body, empty PATCH body, invalid primary key format {error, parameter?, column?}
401 Unauthorized No valid authentication credential presented {error}
403 Forbidden Valid credential but insufficient permissions {error}
404 Not Found No row matches the primary key (GET by ID, UPDATE, DELETE) {error}
405 Method Not Allowed HTTP method not mapped to a route (e.g., DELETE /api/{schema}/{table} on the collection route, or PATCH/PUT/DELETE without {id}) {error}
409 Conflict Unique constraint violation (create/update), foreign key violation (delete) {error, constraint?, column?}
413 Payload Too Large Request body exceeds 1MB limit {error}
422 Unprocessable Entity Value coercion failure, NOT NULL violation, check constraint violation, foreign key violation (create/update) {error, constraint?, column?}
429 Too Many Requests Rate limit exceeded {error} + rate limit headers
500 Internal Server Error Database error (generic message, logged with details) {error}

Database error classification

Database errors are classified by the provider's exception classifier and mapped to appropriate HTTP status codes:

Database error kind HTTP status Typical cause
UniqueViolation 409 Duplicate unique key/constraint
ForeignKeyViolation (on DELETE) 409 Row is still referenced
ForeignKeyViolation (on CREATE/UPDATE) 422 Referenced row does not exist
NotNullViolation 422 Required column is missing or null
CheckViolation 422 Value violates a check constraint
Other 422 Value is not valid (too long, out of range, wrong format)

Rate limiting

Every generated API enforces four independent rate limits in order. A request is rejected as soon as it exceeds the first limit it encounters; it never consumes quota from limits checked later.

Limit Scope Window Key When applied
Platform global Entire deployment Configurable (default 60s) ratelimit:platform:global Every request to every tenant/API
Platform tenant One organization Configurable (default 60s) ratelimit:platform:tenant:{orgId} Every request to any API of that org
Caller quota One authenticated caller 1 minute Per API key, JWT subject, or mTLS cert All routes of the API
Endpoint limit One route + caller 1 minute Per endpoint path + caller That specific endpoint

Platform limits (#188) are fleet-wide and checked first so a refused request never consumes caller or endpoint quota.

Rate limit headers on every response

Every successful response and every 429 rejection carries the three standard rate-limit headers for the quota closest to running out:

  • X-RateLimit-Limit — the quota ceiling for that window
  • X-RateLimit-Remaining — requests left in the current window
  • X-RateLimit-Reset — seconds until the window resets

On a 429 response only, you also receive:

  • Retry-After — seconds to wait before retrying (same value as X-RateLimit-Reset)
  • X-RateLimit-Scope — which limit tripped: global, tenant, caller, or endpoint

Example 429 response:

HTTP
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 42
X-RateLimit-Scope: caller

{"error": "Rate limit exceeded. Try again later."}

Caller quota defaults

  • API keys: Each key has its own RateLimitPerMinute (default: 60)
  • JWT/mTLS: The authentication method's configured quota, or 60 if not set
  • Anonymous (Public endpoints): No caller quota; only platform and endpoint limits apply

Backing store

When a distributed store (Redis) is configured, counters are fleet-wide across all API runtime replicas. If the store is unavailable or not configured, the middleware fails open to in-process counters so requests are never rejected due to store unavailability; the quota then applies per replica until the store recovers.


OpenAPI documents

Every generated API serves two OpenAPI 3.0.3 documents:

Path Document Description
/openapi.json Plain Describes the API surface exactly: every served operation with stable operationId, list parameters, request schemas, every status code, per-operation security, and x-restward-* extensions
/openapi-enhanced.json Enhanced The plain document plus organization documentation settings: info.description, contact, per-property examples, and authored request/response examples

What the documents contain

Paths:

  • One path per table: /api/{schema}/{table}
  • One path per table with {id}: /api/{schema}/{table}/{id}
  • Custom endpoints appear as additional paths

Operations per path:

  • GET — list (with parameters) and get-by-ID
  • POST — create
  • PUT — update
  • PATCH — partial update
  • DELETE — delete

Parameters (list operation):

  • limit (integer, default 100, min 1, max 1000)
  • offset (integer, default 0, min 0)
  • sort (string, comma-separated columns with optional - prefix)
  • fields (string, comma-separated column names)
  • count (boolean, default false)
  • One filter parameter per column (equality) plus operator syntax parameters

Schemas:

  • {schema}_{table} — the row schema (all columns, read-only for key/generated columns)
  • {schema}_{table}_create — create request body (writable columns, NOT NULL required)
  • {schema}_{table}_update — update/PATCH request body (updatable columns, minProperties: 1)
  • {schema}_{table}_page — list response with count (total + items)
  • {schema}_{table}_created — create response (id object)
  • Error — shared error schema

Security schemes:

  • ApiKeyAuth — X-API-Key header
  • BearerAuth — Authorization: Bearer header (only if JWT is enabled)

Extensions:

  • x-restward-filter-operators — the 8 supported operators
  • x-restward-rate-limit-per-minute — endpoint-specific rate limit
  • x-restward-cache-ttl-seconds — endpoint-specific cache TTL

Responses:

  • All operations document: 200, 201, 204, 400, 401, 403, 404, 405, 409, 413, 422, 429, 500
  • Rate limit headers are documented on all successful responses

Examples

See the examples section for practical, language-specific examples:


Issue #165 vs today: discrepancy list

The original issue #165 (2026-09-28) contained several statements that have since changed in the codebase. This reference documents the current behavior as implemented in the code.

Issue statement Current behavior Code location
"no rate-limit headers today (#152)" Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) are present on every response (success and 429) for the closest quota ApiKeyRateLimitingMiddleware.WriteHeaders, ApiKeyRateLimitingMiddleware.WriteRateLimitedAsync
Retry-After and X-RateLimit-Scope are present on 429 responses ApiKeyRateLimitingMiddleware.WriteRateLimitedAsync
Four independent limits are enforced (platform global, platform tenant, caller quota, endpoint limit) ApiKeyRateLimitingMiddleware.InvokeAsync
"fields is accepted but ignored (#153)" fields is fully implemented: comma-separated column names for projection; unknown or empty is a 400 FilterParser.ParseFields
count=true is fully implemented: returns {total, items} and X-Total-Count header FilterParser.ParseCount, DynamicQueryHandler.HandleListAsync
"unknown columns/operators are silently dropped (#151)" Unknown filter columns or operators return 400 naming the parameter FilterParser.ParseFilters (throws QueryParameterException)
"PUT is partial update, no PATCH" PATCH is implemented (#153): strict partial update; PUT is full update (unknown/key/generated columns ignored) DynamicRouteRegistrar.StandardRoutes, DynamicQueryHandler.HandlePatchAsync
"eight filter operators" Confirmed: eq, neq, gt, gte, lt, lte, like, in FilterParser.ParseFilters, DynamicOpenApiGenerator.FilterOperators
"no rate-limit headers today" Rate limit headers are documented in the OpenAPI spec DynamicOpenApiGenerator.AddRateLimitHeaders

See also

Reconnecting to server...

Please wait while we restore your connection

Connection lost

Retrying in seconds...

Unable to reconnect

The server connection has been lost.
You may need to log in again.

Session Paused

The server has temporarily paused your session.

Session Expired

Your session could not be resumed.
Please reload the page to continue.