Tabular Data
Tabular Data API
Section titled “Tabular Data API”The Customer Tabular Data API lets the Cellosign platform retrieve tabular data directly from a service you host in your own environment. You implement a single HTTP endpoint that returns rows and columns on request, and the platform reads from it as an HTTP client. The API is deliberately independent of your storage technology: a requested collection can be a database table, a view, a materialized view, a report, a logical dataset, an API-backed dataset, or any other source you can represent as rows and columns.
Why do I need this?
Section titled “Why do I need this?”Use this API when the platform needs to read structured data that lives in your environment, without exposing your database directly. It is useful when:
- Data cannot be entered into a form and must instead be fetched from your systems (for example a CRM, ERP, data warehouse, or reporting layer).
- You want to expose a curated, read-only view of business data (such as orders or customers) while keeping full control over what is reachable.
- The platform needs to page through a dataset in a stable, repeatable way.
The operation is read-only. You keep an allow-list of exactly which collections and fields are reachable, so the platform can only see data you have explicitly exposed.
Endpoint
Section titled “Endpoint”The operation is an HTTP POST request with JSON request and response bodies.
The path is chosen by you. The platform does not require /v1/data/query, /data/query, or any other specific path — the complete endpoint URL is supplied separately as deployment configuration for each integration. For example, all of the following are valid deployments of the same contract:
POST https://customer.example.com/data/queryPOST https://customer.example.com/integrations/platform/exportPOST https://api.customer.example.com/custom/path/tabular-dataPOST is used (even though the operation is read-only) because the query definition is structured and can be too large or complex for URL query parameters. A successful request returns a single page of rows as JSON.
Request
Section titled “Request”Following are the definitions for the body request posted from Cellosign to your API.
| Element | Type | Required | What it’s for |
|---|---|---|---|
collection |
string | Yes | Logical name of the requested tabular source. It does not have to be a physical database table name. |
fields |
array of strings | Yes | Fields/columns that must be returned. At least one field is required. Duplicate names are not allowed. |
sort |
array of objects | No | Ordered sort keys. The first element has the highest sort priority. Default: empty list. |
sort[].field |
string | Yes | Field used for sorting. |
sort[].direction |
asc or desc |
Yes | Sort direction. |
offset |
integer >= 0 | Yes | Zero-based number of sorted rows to skip. |
limit |
integer 1..100000 | Yes | Maximum number of rows to return. |
The API contract supports a maximum limit of 100,000 rows per request. A deployment may choose a lower operational maximum because of memory, response-size, database, or gateway constraints. If it does, that maximum must be documented as deployment configuration, and requests above it should be rejected with HTTP 422 and a stable INVALID_LIMIT error rather than silently truncated.
Example post request
Section titled “Example post request”{ "collection": "orders", "fields": [ "id", "customer_id", "status", "created_at", "total" ], "sort": [ {"field": "created_at", "direction": "asc"}, {"field": "id", "direction": "asc"} ], "offset": 0, "limit": 1000}Collection and field names
Section titled “Collection and field names”The platform treats collection and field names as opaque identifiers. It does not assume SQL syntax and does not require the names to correspond directly to physical database objects. Your implementation is responsible for mapping these identifiers to the underlying data source, for example:
"orders" -> approved application query / database view / repository"customers" -> approved application query / database view / repositoryDo not concatenate collection, fields, or sort[].field directly into SQL received from the request. Keep an allow-list of exposed collections and allowed fields. This prevents SQL injection and prevents the integration from accessing data that was not explicitly exposed.
Sorting and deterministic pagination
Section titled “Sorting and deterministic pagination”Offset pagination is reliable only when the sort order is deterministic. For example, this is potentially unstable, because multiple records may share the same created_at value:
"sort": [ {"field": "created_at", "direction": "asc"}]Prefer adding a unique, stable tie-breaker such as id:
"sort": [ {"field": "created_at", "direction": "asc"}, {"field": "id", "direction": "asc"}]The effective semantics are equivalent to the following (this is semantic pseudocode, not a requirement to use SQL):
FROM collectionSELECT fieldsORDER BY sort[0], sort[1], ...OFFSET offsetLIMIT limitIf rows are inserted, deleted, or updated while the platform is reading multiple pages, traditional offset pagination can naturally produce duplicated or skipped records. For integrations that require a transactionally consistent full export, your service should provide consistency at the backing-store level — for example by querying a snapshot, an immutable export dataset, or an equivalent application-specific mechanism. Snapshot management is outside version 1 of this API contract.
Expected response
Section titled “Expected response”The response explicitly describes the schema of the returned fields in columns. This is intentional: the platform should not have to infer a column type from the first non-null value in rows.
| Element | Type | What it’s for |
|---|---|---|
collection |
string | Requested collection name. |
columns |
array of objects | Ordered schema of the returned fields. Must correspond one-to-one and in the same order as request fields. |
columns[].name |
string | Field name and JSON key used in every row. |
columns[].type |
enum | API-level field type. See type mapping below. |
columns[].nullable |
boolean | Whether JSON null is allowed for this field. |
rows |
array of objects | Result rows. Every row must contain all keys declared in columns. |
page.offset |
integer | Request offset. |
page.limit |
integer | Request limit. |
page.returned |
integer | Number of elements in rows. |
page.has_more |
boolean | Whether another page exists for the same query definition. |
The following invariants must hold:
columns.length == request.fields.lengthcolumns[i].name == request.fields[i]page.returned == rows.lengthEvery rows[] object must contain every columns[].name key. If the source value is absent or null, return the key with JSON null.
When page.has_more is true, the next request should normally use next_offset = page.offset + page.returned and exactly the same collection, fields, and sort values. The platform must stop when has_more == false.
Example response
Section titled “Example response”{ "collection": "orders", "columns": [ {"name": "id", "type": "integer", "nullable": false}, {"name": "customer_id", "type": "string", "nullable": false}, {"name": "status", "type": "string", "nullable": true}, {"name": "created_at", "type": "datetime", "nullable": false}, {"name": "total", "type": "decimal", "nullable": true} ], "rows": [ { "id": 1001, "customer_id": "C-42", "status": "PAID", "created_at": "2026-08-11T12:00:00Z", "total": "159.90" }, { "id": 1002, "customer_id": "C-77", "status": "NEW", "created_at": "2026-08-11T12:01:15Z", "total": "49.00" } ], "page": { "offset": 0, "limit": 1000, "returned": 2, "has_more": false }}Column types and JSON value mapping
Section titled “Column types and JSON value mapping”Explicit field types simplify platform-side parsing, validation, null handling, and conversion. Your service must return one of the following API-level types for each column.
columns[].type |
JSON representation | Meaning / examples |
|---|---|---|
string |
JSON string | Text, or another value intentionally exposed as text. |
integer |
JSON number | Integral value. Values should fit the integer range supported by the agreed platform implementation. |
number |
JSON number | Floating-point numeric value where normal JSON/IEEE-754 semantics are acceptable. |
decimal |
JSON string | Exact decimal value, e.g. "159.90". String encoding avoids precision loss. |
boolean |
JSON boolean | true / false. |
date |
JSON string | ISO 8601 calendar date, e.g. "2026-08-11". |
datetime |
JSON string | RFC 3339 timestamp, preferably UTC, e.g. "2026-08-11T12:00:00Z". |
uuid |
JSON string | UUID textual representation. |
binary |
JSON string | Base64-encoded bytes. |
json |
any JSON value | Nested JSON object, array, scalar, or null when the source exposes structured JSON. |
Why decimal is a string. A decimal database type often represents exact business values such as money. Encoding it as a JSON number can lose precision in consumers that use IEEE-754 floating point. The contract therefore declares {"name": "total", "type": "decimal", "nullable": false} and returns a row value such as {"total": "159.90"}, so the platform can parse it using an arbitrary-precision decimal implementation.
Nulls. If nullable is true, the row value may be JSON null regardless of the declared non-null type (for example {"status": null}). If nullable is false, your service must not return JSON null for that field.
Authentication
Section titled “Authentication”Authentication is selected per customer deployment. One of the following application-level modes may be configured:
- No HTTP authentication.
- HTTP Basic authentication.
- Bearer token authentication.
- API key in an HTTP header.
mTLS may additionally be enabled as transport-level client authentication. All authentication parameters are deployment configuration; they are not transmitted inside the data-query request body.
No authentication
Section titled “No authentication”No authorization header is sent. This mode should only be used where network-level controls make unauthenticated access acceptable — for example a private network with strict firewall rules.
HTTP Basic
Section titled “HTTP Basic”Authorization: Basic <base64(username:password)>Credentials are configured separately and must be transmitted only over HTTPS.
Bearer token
Section titled “Bearer token”The data request contains:
Authorization: Bearer <access-token>The access token may be provided to the platform directly, or the platform may obtain it from a customer identity provider before calling the data endpoint. A common configuration is OAuth 2.0 Client Credentials:
Token endpoint: https://identity.customer.example.com/oauth2/tokenClient ID: configured separatelyClient secret: configured separatelyScopes: optional, configured separatelyConceptually, before calling the data API the platform performs a standard token request equivalent to:
POST /oauth2/tokenContent-Type: application/x-www-form-urlencodedAuthorization: Basic <base64(client_id:client_secret)>
grant_type=client_credentials&scope=<optional-scopes>The exact OAuth token endpoint, client authentication method, client ID, client secret, scopes, audience/resource parameter, and any other identity-provider-specific fields are configured separately and are outside the tabular-data API contract. The resulting access token is then sent to the configured data endpoint as a normal Bearer token.
API key header
Section titled “API key header”Default header defined by the OpenAPI contract:
X-API-Key: <api-key>The header name is also deployment configuration. X-API-Key is the default/example name; you may require another header such as Authorization-Key or a vendor-specific name, and the platform is configured accordingly.
Mutual TLS (mTLS)
Section titled “Mutual TLS (mTLS)”mTLS is performed during the TLS handshake, before the HTTP request is processed. When mTLS is enabled:
- your service presents its normal HTTPS server certificate;
- the platform presents a client certificate;
- your endpoint validates the platform client certificate against the agreed CA/trust chain;
- certificate expiry and revocation procedures must be operationally defined;
- TLS 1.2 or newer is required; TLS 1.3 is preferred where supported.
mTLS may be used alone or together with Basic, Bearer, or API-key authentication, depending on the deployment agreement.
OpenAPI 3.0 limitation: OpenAPI 3.0.x does not define a standard
mutualTLSsecurity scheme, so the accompanyingopenapi.yamlusesx-mtls-supported: true. OpenAPI 3.1 has a standardtype: mutualTLSsecurity scheme, but this contract intentionally targets OpenAPI 3.0.3.
HTTP headers
Section titled “HTTP headers”Request — required:
Content-Type: application/jsonAccept: application/jsonRequest — recommended:
X-Request-ID: <unique-correlation-id>If the platform sends X-Request-ID, your service should preserve it in logs and return the same value in the response. If no request ID is supplied, the service may generate one.
Response — recommended:
Content-Type: application/jsonX-Request-ID: <correlation-id>Errors
Section titled “Errors”Errors use a common JSON structure:
{ "error": { "code": "INVALID_FIELD", "message": "Field 'legacy_code' does not exist in collection 'orders'.", "request_id": "01J5F7M8Z7QY8K3YF8CE6R4M8A", "details": {} }}error.code is intended for programmatic handling and must remain stable. error.message is for diagnostics and may change.
HTTP status codes
Section titled “HTTP status codes”| Status | Meaning |
|---|---|
200 |
Query completed successfully. |
400 |
Malformed JSON or structurally invalid request. |
401 |
Authentication is missing or invalid. |
403 |
Authenticated caller is not allowed to access the requested data. |
404 |
Collection does not exist or is not exposed. |
422 |
Request is structurally valid but contains an unsupported/invalid collection, field, sort field, limit, or another semantic error. |
429 |
Rate limit exceeded. |
500 |
Unexpected server-side error. |
503 |
Service or backing data source is temporarily unavailable. |
Recommended stable error codes: INVALID_REQUEST, UNAUTHORIZED, FORBIDDEN, COLLECTION_NOT_FOUND, INVALID_FIELD, INVALID_SORT_FIELD, INVALID_LIMIT, RATE_LIMITED, INTERNAL_ERROR, SERVICE_UNAVAILABLE.
For 429 and temporary 503 responses, the service should return Retry-After when a meaningful retry delay is known.
Retries
Section titled “Retries”The configured POST data endpoint is logically read-only and must not modify customer data. The platform may retry a request after network failures or transient HTTP errors, so your implementation must make this endpoint safe to repeat with the same body.
Recommended retryable conditions:
- network connection failure;
- timeout before a complete response is received;
- HTTP
429; - HTTP
502, if generated by your infrastructure; - HTTP
503; - HTTP
504, if generated by your infrastructure.
HTTP 400, 401, 403, 404, and 422 should not be retried without changing configuration, credentials, or the request.
Timeouts and performance
Section titled “Timeouts and performance”Your service should stream data from its backing source internally and avoid loading an entire collection into memory merely to produce one API page. Recommended operational requirements, unless a separate SLA is agreed:
- support protocol
limitvalues up to 100,000 rows; - return JSON using UTF-8;
- support HTTP keep-alive;
- support gzip/br compression at the HTTP infrastructure layer where appropriate;
- avoid unnecessary response buffering that can cause excessive memory consumption;
- index or otherwise optimize the fields used for stable sorting on large collections.
A 100,000-row response can be large. You and the platform should agree request timeout, maximum response body size, rate limits, concurrency limits, and any lower operational page-size limit as deployment configuration.
Security requirements
Section titled “Security requirements”Your implementation should follow these rules:
- HTTPS must be used for all production traffic.
- Never build SQL by directly concatenating request values.
- Maintain an allow-list of exposed collections.
- Maintain an allow-list of selectable/sortable fields for each collection.
- Apply least-privilege permissions to the backing database/service account.
- Do not expose secrets, SQL statements, stack traces, or internal connection details in error responses.
- Log authentication and authorization failures without logging passwords, OAuth client secrets, bearer tokens, API keys, or private-key material.
- Rate-limit the endpoint when it is reachable from shared or public networks.
- Rotate Basic passwords, OAuth client secrets, statically provisioned bearer credentials, API keys, and mTLS certificates according to the deployment security policy.
Example curl requests
Section titled “Example curl requests”All examples below use the illustrative URL https://customer.example.com/data/query. Replace it with your configured endpoint URL, including your chosen path.
No authentication
Section titled “No authentication”curl --fail-with-body \ --request POST \ 'https://customer.example.com/data/query' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data '{ "collection": "orders", "fields": ["id", "status", "created_at"], "sort": [ {"field": "created_at", "direction": "asc"}, {"field": "id", "direction": "asc"} ], "offset": 0, "limit": 1000 }'Basic authentication
Section titled “Basic authentication”curl --fail-with-body \ --user 'api-user:api-password' \ --request POST \ 'https://customer.example.com/data/query' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data @request.jsonBearer token
Section titled “Bearer token”curl --fail-with-body \ --request POST \ 'https://customer.example.com/data/query' \ --header 'Authorization: Bearer REPLACE_WITH_TOKEN' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data @request.jsonOAuth 2.0 Client Credentials + Bearer token
Section titled “OAuth 2.0 Client Credentials + Bearer token”Token acquisition example; the exact token endpoint and parameters are configuration:
ACCESS_TOKEN="$( curl --fail --silent --show-error \ --user "$CLIENT_ID:$CLIENT_SECRET" \ --request POST \ 'https://identity.customer.example.com/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'scope=tabular.read' \ | jq -r '.access_token')"Then call the configured data endpoint:
curl --fail-with-body \ --request POST \ 'https://customer.example.com/data/query' \ --header "Authorization: Bearer $ACCESS_TOKEN" \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data @request.jsonAPI key
Section titled “API key”curl --fail-with-body \ --request POST \ 'https://customer.example.com/data/query' \ --header 'X-API-Key: REPLACE_WITH_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data @request.jsonmTLS + Bearer
Section titled “mTLS + Bearer”curl --fail-with-body \ --cert './client.crt' \ --key './client.key' \ --cacert './customer-ca.crt' \ --request POST \ 'https://customer.example.com/data/query' \ --header 'Authorization: Bearer REPLACE_WITH_TOKEN' \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data @request.jsonMinimal conformance examples
Section titled “Minimal conformance examples”Empty page
Section titled “Empty page”Request:
{ "collection": "orders", "fields": ["id"], "sort": [{"field": "id", "direction": "asc"}], "offset": 500000, "limit": 1000}Response:
{ "collection": "orders", "columns": [ {"name": "id", "type": "integer", "nullable": false} ], "rows": [], "page": { "offset": 500000, "limit": 1000, "returned": 0, "has_more": false }}Invalid field
Section titled “Invalid field”HTTP status: 422 Unprocessable Entity
{ "error": { "code": "INVALID_FIELD", "message": "Field 'unknown' does not exist in collection 'orders'." }}Implementation checklist
Section titled “Implementation checklist”Before the endpoint is considered ready for integration, verify all of the following:
- Your
POSTendpoint URL and path are configured in the platform and reachable from the platform network. - Production endpoint uses HTTPS.
- The agreed authentication mode works.
- If OAuth 2.0 Client Credentials is used, token URL, client ID, client secret, scopes/audience, and client-authentication method are configured and token acquisition works.
- mTLS works if required.
- Only explicitly approved collections are exposed.
- Only explicitly approved fields can be selected or sorted.
- Unknown collection returns
404withCOLLECTION_NOT_FOUND. - Unknown field returns
422withINVALID_FIELDorINVALID_SORT_FIELD. -
offset = 0returns the first page. -
limit = 1works. - Protocol
limit = 100000is supported, or a documented lower deployment limit is enforced with422andINVALID_LIMIT. - Stable multi-column sorting works in both
ascanddescdirections. -
columnscontains exactly the requested fields and preserves request order. - Every row contains every requested field key.
- Every row value conforms to the corresponding declared
columns[].type. -
nullable: falsefields never contain JSONnull. - Exact decimal values are returned as strings with
type: decimal. -
page.returned == rows.lengthfor every response. -
has_morecorrectly indicates whether another page exists. - Empty result returns HTTP
200,rows: [],returned: 0, andhas_more: falsewhile still returningcolumns. - Timestamps are returned in RFC 3339 format.
- The endpoint does not modify data.
- Repeating an identical request is safe.
- Authentication secrets never appear in application logs.
- Error responses do not expose stack traces or internal credentials.
- Correlation/request IDs are logged and preferably returned as
X-Request-ID.
Deployment configuration
Section titled “Deployment configuration”The following values are intentionally outside the protocol payload and are configured per customer integration:
| Configuration | Examples |
|---|---|
| Data endpoint URL | https://api.customer.example.com/integrations/tabular |
| Authentication mode | none / Basic / Bearer / API key |
| Basic credentials | username + password |
| Static Bearer credential | token, when applicable |
| OAuth token endpoint | https://idp.customer.example.com/oauth2/token |
| OAuth client ID | customer-issued client ID |
| OAuth client secret | customer-issued client secret |
| OAuth scopes / audience / resource | identity-provider specific |
| API-key header name | X-API-Key or customer-specific header |
| API-key value | customer-issued secret |
| mTLS settings | client certificate/private key, trusted server CA as applicable |
| Timeout | deployment-specific |
| Rate/concurrency limits | deployment-specific |
| Operational max page size | up to the protocol maximum of 100,000 |