Skip to content

Latest commit

 

History

History
557 lines (446 loc) · 16 KB

File metadata and controls

557 lines (446 loc) · 16 KB
title Wire Format & JSON Examples
description Complete HTTP request/response examples for ObjectStack API operations — CRUD, queries, metadata, errors, and batch operations

Wire Format & JSON Examples

This page provides complete HTTP request and response examples for every common ObjectStack API operation. All examples use a task object with realistic data.

**Base URL:** All endpoints are relative to your ObjectStack instance, e.g. `https://api.example.com`. **Content-Type:** All requests and responses use `application/json`. **Authentication:** Include `Authorization: Bearer ` header on all requests.

1. Create Record

POST /api/v1/data/task

Creates a new record on the task object.

Request

The request body is the record — field values are sent at the top level, not wrapped in a data envelope.

{
  "title": "Implement login page",
  "description": "Build the authentication UI with email/password and OAuth support",
  "status": "open",
  "priority": "high",
  "assigned_to": "usr_01HQ3V5K8N2M4P6R7T9W",
  "due_date": "2025-03-15",
  "estimated_hours": 8,
  "tags": ["frontend", "auth"],
  "project": "prj_01HQ3V5K8N2M4P6R7T9X"
}

Response — 201 Created

The response is the CreateDataResponse envelope: { object, id, record }.

{
  "object": "task",
  "id": "tsk_01HQ4A7B9D3F5G8J2K4L",
  "record": {
    "id": "tsk_01HQ4A7B9D3F5G8J2K4L",
    "title": "Implement login page",
    "description": "Build the authentication UI with email/password and OAuth support",
    "status": "open",
    "priority": "high",
    "assigned_to": "usr_01HQ3V5K8N2M4P6R7T9W",
    "due_date": "2025-03-15",
    "estimated_hours": 8,
    "tags": ["frontend", "auth"],
    "project": "prj_01HQ3V5K8N2M4P6R7T9X",
    "created_at": "2025-01-20T10:30:00.000Z",
    "updated_at": "2025-01-20T10:30:00.000Z",
    "created_by": "usr_01HQ3V5K8N2M4P6R7T9W",
    "owner_id": "usr_01HQ3V5K8N2M4P6R7T9W"
  }
}

2. Read Record

GET /api/v1/data/task/tsk_01HQ4A7B9D3F5G8J2K4L

Retrieves a single record by ID.

Request

No request body. Optional query parameters:

Parameter Type Description
select string Comma-separated list of fields to return
expand string Comma-separated lookup fields to expand
GET /api/v1/data/task/tsk_01HQ4A7B9D3F5G8J2K4L?select=title,status,assigned_to&expand=assigned_to

Response — 200 OK

The response is the GetDataResponse envelope: { object, id, record }.

{
  "object": "task",
  "id": "tsk_01HQ4A7B9D3F5G8J2K4L",
  "record": {
    "id": "tsk_01HQ4A7B9D3F5G8J2K4L",
    "title": "Implement login page",
    "status": "open",
    "assigned_to": {
      "id": "usr_01HQ3V5K8N2M4P6R7T9W",
      "name": "Jane Smith",
      "email": "jane@example.com"
    }
  }
}

3. List Records with Query

POST /api/v1/data/task/query

Executes a structured query (a QueryAST) against the task object.

Request

Filtering uses the MongoDB-style where clause: { field: value } for equality and { field: { $op: value } } for operators. Combine clauses with $and / $or / $not. expand is a map of relationship-field name → nested query.

{
  "where": {
    "$and": [
      { "status": { "$in": ["open", "in_progress"] } },
      { "priority": "high" },
      { "due_date": { "$lte": "2025-03-31" } }
    ]
  },
  "fields": ["id", "title", "status", "priority", "assigned_to", "due_date"],
  "expand": {
    "assigned_to": { "object": "sys_user", "fields": ["id", "name"] }
  },
  "orderBy": [
    { "field": "due_date", "order": "asc" }
  ],
  "limit": 20,
  "offset": 0
}

Response — 200 OK

The response is the FindDataResponse envelope: { object, records, total?, hasMore? }.

{
  "object": "task",
  "records": [
    {
      "id": "tsk_01HQ4A7B9D3F5G8J2K4L",
      "title": "Implement login page",
      "status": "open",
      "priority": "high",
      "assigned_to": {
        "id": "usr_01HQ3V5K8N2M4P6R7T9W",
        "name": "Jane Smith"
      },
      "due_date": "2025-03-15"
    },
    {
      "id": "tsk_01HQ4B8C0E4G6H9K3L5M",
      "title": "Fix payment processing bug",
      "status": "in_progress",
      "priority": "high",
      "assigned_to": {
        "id": "usr_01HQ3W6L9O3N5Q7S8U0X",
        "name": "Bob Johnson"
      },
      "due_date": "2025-03-20"
    }
  ],
  "total": 47,
  "hasMore": true
}
**Field Operators:** `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$between`, `$contains`, `$notContains`, `$startsWith`, `$endsWith`, `$null`, `$exists`. Combine clauses with `$and` / `$or` / `$not` for complex queries.

4. Update Record

PATCH /api/v1/data/task/tsk_01HQ4A7B9D3F5G8J2K4L

Updates specific fields on an existing record. Only include fields you want to change. As with create, the changed fields are sent at the top level — not wrapped in a data envelope.

Request

{
  "status": "in_progress",
  "estimated_hours": 12,
  "tags": ["frontend", "auth", "urgent"]
}
**Optimistic concurrency:** Pass the `updated_at` value you last read as an `If-Match` request header (or an `expectedVersion` field in the body) and the server returns `409 CONCURRENT_UPDATE` if the record changed in the meantime.

Response — 200 OK

The response is the UpdateDataResponse envelope: { object, id, record }.

{
  "object": "task",
  "id": "tsk_01HQ4A7B9D3F5G8J2K4L",
  "record": {
    "id": "tsk_01HQ4A7B9D3F5G8J2K4L",
    "title": "Implement login page",
    "description": "Build the authentication UI with email/password and OAuth support",
    "status": "in_progress",
    "priority": "high",
    "assigned_to": "usr_01HQ3V5K8N2M4P6R7T9W",
    "due_date": "2025-03-15",
    "estimated_hours": 12,
    "tags": ["frontend", "auth", "urgent"],
    "project": "prj_01HQ3V5K8N2M4P6R7T9X",
    "created_at": "2025-01-20T10:30:00.000Z",
    "updated_at": "2025-01-20T14:15:00.000Z",
    "created_by": "usr_01HQ3V5K8N2M4P6R7T9W",
    "owner_id": "usr_01HQ3V5K8N2M4P6R7T9W"
  }
}

5. Delete Record

DELETE /api/v1/data/task/tsk_01HQ4A7B9D3F5G8J2K4L

Permanently deletes a record.

Request

No request body.

Response — 200 OK

The response is the DeleteDataResponse envelope: { object, id, success }.

{
  "object": "task",
  "id": "tsk_01HQ4A7B9D3F5G8J2K4L",
  "success": true
}

6. Get Metadata

GET /api/v1/meta/object

Metadata is addressed by type name. object is the metadata type, so this route lists every registered object the current user can access. The response is an { type, items } envelope.

Response — 200 OK

{
  "type": "object",
  "items": [
    {
      "name": "task",
      "label": "Task",
      "pluralLabel": "Tasks",
      "description": "Project tasks and work items",
      "fields": [
        {
          "name": "title",
          "label": "Title",
          "type": "text",
          "required": true,
          "maxLength": 200
        },
        {
          "name": "status",
          "label": "Status",
          "type": "select",
          "required": true,
          "options": [
            { "label": "Open", "value": "open" },
            { "label": "In Progress", "value": "in_progress" },
            { "label": "Done", "value": "done" },
            { "label": "Cancelled", "value": "cancelled" }
          ],
          "defaultValue": "open"
        },
        {
          "name": "assigned_to",
          "label": "Assigned To",
          "type": "lookup",
          "reference": "sys_user"
        },
        {
          "name": "due_date",
          "label": "Due Date",
          "type": "date"
        }
      ],
      "enable": {
        "apiEnabled": true,
        "trackHistory": true,
        "searchable": true
      }
    }
  ]
}

Single Object Metadata

GET /api/v1/meta/object/task

Returns metadata for a single object including all fields, relationships, and configuration.


7. Error Response Format

Data route errors are returned as a flat envelope: a top-level error message string, a machine-readable code, and operation-specific extras (fields, object, currentVersion, …). The HTTP status is carried by the response, not duplicated in the body.

Validation Error — 400 Bad Request

Field-level failures carry a fields array.

{
  "error": "title is required",
  "code": "VALIDATION_FAILED",
  "fields": [
    {
      "field": "title",
      "code": "required",
      "message": "title is required"
    }
  ],
  "object": "task"
}

Not Found — 404 Not Found

{
  "error": "Record tsk_01HQ4A7B9D3F5G8J2K4L not found in task",
  "code": "RECORD_NOT_FOUND",
  "object": "task"
}

Permission Denied — 403 Forbidden

{
  "error": "[Security] Access denied: operation 'update' on object 'task' is not permitted for positions [standard_user]",
  "code": "PERMISSION_DENIED",
  "object": "task"
}

Concurrent Update — 409 Conflict

Returned when an If-Match / expectedVersion token no longer matches the stored record.

{
  "error": "Record was modified by another user",
  "code": "CONCURRENT_UPDATE",
  "currentVersion": "2025-01-20T14:15:00.000Z",
  "object": "task"
}

Datasource Unavailable — 503 Service Unavailable

Returned when the object's declared datasource has no live driver: the host's connect policy refused it, or it failed to connect at startup and the server was started with OS_ALLOW_DRIVER_CONNECT_FAILURE. Nothing about the request is wrong, and the state may clear — so this is a 503, not a 400 or a 500.

reason is the class (blocked | failed). The underlying cause is deliberately not included — it routinely contains hosts, ports and DSNs; it stays in the server logs and Setup → Datasources. See External Datasources.

{
  "error": "[ObjectQL] Datasource 'analytics' configured for object 'visit' is declared but not connected: the host's datasource connect policy refused it. See the startup logs or Setup → Datasources for the cause.",
  "code": "ERR_DATASOURCE_UNAVAILABLE",
  "datasource": "analytics",
  "reason": "blocked",
  "object": "visit"
}
**Error Codes:** See the [Error Catalog](/docs/api/error-catalog) for the complete list of error codes and their meanings.

Service-mounted routes use the declared envelope

The flat envelope above is what the kernel REST server emits for /api/v1/data/*. Routes mounted directly by a service plugin — /api/v1/storage/* and /api/v1/i18n/* — instead return the BaseResponse shape their contract declares, with the code inside the error object:

{
  "success": false,
  "error": {
    "code": "ATTACHMENT_DOWNLOAD_DENIED",
    "message": "You do not have access to a record this file is attached to"
  }
}

Read body.error.code, not body.code, on these routes. ObjectStackClient normalizes both — error.code and error.message are populated whichever envelope the server used — so SDK callers do not need to branch.

Dispatcher routes use it too

Everything the runtime dispatcher serves — /api/v1/meta/*, /actions/*, /packages/*, /automation/*, /analytics/*, /ready, … — answers in the same declared envelope, plus httpStatus:

{
  "success": false,
  "error": {
    "code": "PROJECT_MEMBERSHIP_REQUIRED",
    "message": "Forbidden: user usr_01H… is not a member of project env_prod",
    "httpStatus": 403,
    "details": { "environmentId": "env_prod", "userId": "usr_01H…" }
  }
}
Field Meaning
code The semantic code — the field to branch on. Never a number.
message Human-readable text, safe to show. 5xx messages are sanitised.
httpStatus The response status, mirrored. Redundant with the response line by design.
details Structured context only (fields[], issues[], ids). Never a parked code.

A route-resolution failure spells code from DispatcherErrorCode (ROUTE_NOT_FOUND, …) and adds route / hint / service as siblings. A branch with no code of its own gets a StandardErrorCode derived from the status.

Before [#3842](#3842) this envelope put the HTTP status in `error.code` and the real code in `error.details.code`, `error.details.type` or `error.type`. `ObjectStackClient` reads all of them, so an SDK newer than the server it talks to is unaffected; code reading these bodies directly should move to `error.code`.

8. Batch Operations

POST /api/v1/data/task/batch

Process many records of a single operation type in one request. The body carries one operation (create, update, upsert, or delete) plus a records array. By default (options.atomic: true) processing stops at the first failing record — records already written earlier in the same batch are not rolled back, since there is no wrapping database transaction. Set options.atomic: false (with options.continueOnError: true) to keep processing every record and collect a full partial-success report.

Request

{
  "operation": "update",
  "records": [
    { "id": "tsk_01HQ4A7B9D3F5G8J2K4L", "data": { "status": "done" } },
    { "id": "tsk_01HQ4B8C0E4G6H9K3L5M", "data": { "status": "done" } }
  ],
  "options": {
    "atomic": true,
    "continueOnError": false,
    "validateOnly": false
  }
}

Response — 200 OK

The response is the BatchUpdateResponse envelope: a top-level success flag plus total / succeeded / failed counts and a per-record results array. Each successful entry echoes the written record; pass options.returnRecords: false to get back just { id, success } per result.

{
  "success": true,
  "operation": "update",
  "total": 2,
  "succeeded": 2,
  "failed": 0,
  "results": [
    { "id": "tsk_01HQ4A7B9D3F5G8J2K4L", "success": true, "record": { "id": "tsk_01HQ4A7B9D3F5G8J2K4L", "status": "done" } },
    { "id": "tsk_01HQ4B8C0E4G6H9K3L5M", "success": true, "record": { "id": "tsk_01HQ4B8C0E4G6H9K3L5M", "status": "done" } }
  ]
}

Partial Failure Response

When options.atomic: false and some records fail, the failing entries carry a single error message string (not an array):

{
  "success": false,
  "operation": "update",
  "total": 2,
  "succeeded": 1,
  "failed": 1,
  "results": [
    { "id": "tsk_01HQ4A7B9D3F5G8J2K4L", "success": true, "record": { "id": "tsk_01HQ4A7B9D3F5G8J2K4L", "status": "done" } },
    { "id": "tsk_invalid_id", "success": false, "error": "Record tsk_invalid_id not found in task" }
  ]
}
**Batch Limits:** Maximum 200 records per batch request. For larger imports, use the streaming import endpoint `POST /api/v1/data/:object/import`.

Common Headers

Request Headers

Header Required Description
Authorization Yes Bearer <access_token>
Content-Type Yes application/json
X-Request-Id No Client-generated request ID for tracing (honored by the observability dispatcher)
X-Environment-Id No Targets a specific environment/project on unscoped routes
If-Match No Optimistic-concurrency token for PATCH / DELETE (the updated_at you last read)
Accept-Language No Locale for translated labels (e.g., en-US)

Response Headers

Header Description
X-Request-Id Server-assigned (or echoed) request ID
ETag Entity tag for conditional requests
**Rate limiting is opt-in:** ObjectStack ships a token-bucket `RateLimiter` primitive (`@objectstack/runtime`), but it is **not** wired into the default REST response path — a deployment must add it at the adapter layer. Only when a deployment does so will responses carry `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` (and a `429` with `Retry-After` once the limit is hit).