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:
| 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_toResponse — 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
| 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).