Skip to content

Tabular Data

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.

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.

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/query
POST https://customer.example.com/integrations/platform/export
POST https://api.customer.example.com/custom/path/tabular-data

POST 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.

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.

{
"collection": "orders",
"fields": [
"id",
"customer_id",
"status",
"created_at",
"total"
],
"sort": [
{"field": "created_at", "direction": "asc"},
{"field": "id", "direction": "asc"}
],
"offset": 0,
"limit": 1000
}

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 / repository

Do 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.

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 collection
SELECT fields
ORDER BY sort[0], sort[1], ...
OFFSET offset
LIMIT limit

If 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.

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.length
columns[i].name == request.fields[i]
page.returned == rows.length

Every 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.

{
"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
}
}

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 is selected per customer deployment. One of the following application-level modes may be configured:

  1. No HTTP authentication.
  2. HTTP Basic authentication.
  3. Bearer token authentication.
  4. 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 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.

Authorization: Basic <base64(username:password)>

Credentials are configured separately and must be transmitted only over HTTPS.

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/token
Client ID: configured separately
Client secret: configured separately
Scopes: optional, configured separately

Conceptually, before calling the data API the platform performs a standard token request equivalent to:

POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: 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.

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.

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 mutualTLS security scheme, so the accompanying openapi.yaml uses x-mtls-supported: true. OpenAPI 3.1 has a standard type: mutualTLS security scheme, but this contract intentionally targets OpenAPI 3.0.3.

Request — required:

Content-Type: application/json
Accept: application/json

Request — 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/json
X-Request-ID: <correlation-id>

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.

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.

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.

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 limit values 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.

Your implementation should follow these rules:

  1. HTTPS must be used for all production traffic.
  2. Never build SQL by directly concatenating request values.
  3. Maintain an allow-list of exposed collections.
  4. Maintain an allow-list of selectable/sortable fields for each collection.
  5. Apply least-privilege permissions to the backing database/service account.
  6. Do not expose secrets, SQL statements, stack traces, or internal connection details in error responses.
  7. Log authentication and authorization failures without logging passwords, OAuth client secrets, bearer tokens, API keys, or private-key material.
  8. Rate-limit the endpoint when it is reachable from shared or public networks.
  9. Rotate Basic passwords, OAuth client secrets, statically provisioned bearer credentials, API keys, and mTLS certificates according to the deployment security policy.

All examples below use the illustrative URL https://customer.example.com/data/query. Replace it with your configured endpoint URL, including your chosen path.

Terminal window
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
}'
Terminal window
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.json
Terminal window
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.json

OAuth 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:

Terminal window
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:

Terminal window
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.json
Terminal window
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.json
Terminal window
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.json

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
}
}

HTTP status: 422 Unprocessable Entity

{
"error": {
"code": "INVALID_FIELD",
"message": "Field 'unknown' does not exist in collection 'orders'."
}
}

Before the endpoint is considered ready for integration, verify all of the following:

  • Your POST endpoint 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 404 with COLLECTION_NOT_FOUND.
  • Unknown field returns 422 with INVALID_FIELD or INVALID_SORT_FIELD.
  • offset = 0 returns the first page.
  • limit = 1 works.
  • Protocol limit = 100000 is supported, or a documented lower deployment limit is enforced with 422 and INVALID_LIMIT.
  • Stable multi-column sorting works in both asc and desc directions.
  • columns contains 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: false fields never contain JSON null.
  • Exact decimal values are returned as strings with type: decimal.
  • page.returned == rows.length for every response.
  • has_more correctly indicates whether another page exists.
  • Empty result returns HTTP 200, rows: [], returned: 0, and has_more: false while still returning columns.
  • 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.

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