ObjectStackObjectStack

Wire Format & JSON Examples

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

ParameterTypeDescription
selectstringComma-separated list of fields to return
expandstringComma-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
}

Soft Delete: If the object enables soft delete (softDelete: { enabled: true } in its definition), the record is moved to the trash / recycle bin instead of being permanently deleted.


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 roles [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"
}

Error Codes: See the Error Catalog for the complete list of error codes and their meanings.


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

HeaderRequiredDescription
AuthorizationYesBearer <access_token>
Content-TypeYesapplication/json
X-Request-IdNoClient-generated request ID for tracing (honored by the observability dispatcher)
X-Environment-IdNoTargets a specific environment/project on unscoped routes
If-MatchNoOptimistic-concurrency token for PATCH / DELETE (the updated_at you last read)
Accept-LanguageNoLocale for translated labels (e.g., en-US)

Response Headers

HeaderDescription
X-Request-IdServer-assigned (or echoed) request ID
ETagEntity 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).

On this page