Query
Resource querying with filtering, sorting, pagination, and relationships
Extension URN: urn:vnd:ext:query
Overview
Section titled “Overview”The query extension enables rich querying capabilities for collection and resource functions. It bundles filtering, sorting, pagination, sparse fieldsets, and relationship inclusion into a cohesive package.
When enabled, responses MUST use the Resource Object format (type/id/attributes structure) since query features depend on this structure.
When to Use
Section titled “When to Use”Query SHOULD be used for:
- CRUD-style resource APIs
- Collection endpoints returning lists of entities
- APIs requiring filtering, sorting, or pagination
- Graph-like data with relationships between resources
Query SHOULD NOT be used for:
- Simple RPC functions returning scalar values
- Action-oriented endpoints (e.g.,
payments.process) - Functions with custom response structures
Capabilities
Section titled “Capabilities”The query extension provides five capabilities:
| Capability | Description |
|---|---|
| Filtering | Query resources by attribute conditions |
| Sorting | Order results by attributes |
| Pagination | Navigate large result sets |
| Sparse Fieldsets | Request only needed fields |
| Relationships | Include related resources |
Filtering
Section titled “Filtering”Filtering allows clients to request a subset of resources matching specific criteria. Filters are applied server-side before returning results.
Filter Object
Section titled “Filter Object”A filter specifies a single condition:
{ "attribute": "status", "operator": "equals", "value": "pending"}Fields
Section titled “Fields”| Field | Type | Required | Description |
|---|---|---|---|
attribute | string | Yes | Attribute to filter on |
operator | string | Yes | Comparison operator |
value | any | Conditional | Value to compare (type depends on operator) |
boolean | string | No | Boolean combinator (and, or) |
Operators
Section titled “Operators”Equality Operators
Section titled “Equality Operators”| Operator | SQL Equivalent | Value Type | Description |
|---|---|---|---|
equals | = | any | Exact match |
not_equals | != | any | Not equal |
{ "attribute": "status", "operator": "equals", "value": "active" }{ "attribute": "type", "operator": "not_equals", "value": "draft" }Comparison Operators
Section titled “Comparison Operators”| Operator | SQL Equivalent | Value Type | Description |
|---|---|---|---|
greater_than | > | number/string | Greater than |
greater_than_or_equal_to | >= | number/string | Greater than or equal |
less_than | < | number/string | Less than |
less_than_or_equal_to | <= | number/string | Less than or equal |
{ "attribute": "amount", "operator": "greater_than", "value": 100 }{ "attribute": "created_at", "operator": "greater_than_or_equal_to", "value": "2024-01-01T00:00:00Z" }Pattern Operators
Section titled “Pattern Operators”| Operator | SQL Equivalent | Value Type | Description |
|---|---|---|---|
like | LIKE | string | Pattern match (use % wildcard) |
not_like | NOT LIKE | string | Negative pattern match |
{ "attribute": "email", "operator": "like", "value": "%@example.com" }{ "attribute": "name", "operator": "like", "value": "John%" }{ "attribute": "code", "operator": "not_like", "value": "TEST%" }Set Operators
Section titled “Set Operators”| Operator | SQL Equivalent | Value Type | Description |
|---|---|---|---|
in | IN | array | Value in set |
not_in | NOT IN | array | Value not in set |
{ "attribute": "status", "operator": "in", "value": ["pending", "processing", "shipped"] }{ "attribute": "country_code", "operator": "not_in", "value": ["US", "CA"] }Range Operators
Section titled “Range Operators”| Operator | SQL Equivalent | Value Type | Description |
|---|---|---|---|
between | BETWEEN | array[2] | Value in range (inclusive) |
not_between | NOT BETWEEN | array[2] | Value outside range |
{ "attribute": "price", "operator": "between", "value": [10, 100] }{ "attribute": "created_at", "operator": "between", "value": ["2024-01-01", "2024-01-31"] }Null Operators
Section titled “Null Operators”| Operator | SQL Equivalent | Value Type | Description |
|---|---|---|---|
is_null | IS NULL | — | Attribute is null |
is_not_null | IS NOT NULL | — | Attribute is not null |
{ "attribute": "deleted_at", "operator": "is_null" }{ "attribute": "verified_at", "operator": "is_not_null" }Note: value is not required for null operators.
Filter Structure
Section titled “Filter Structure”Filters are organized by resource using an object:
{ "filters": { "self": [ { "attribute": "status", "operator": "equals", "value": "active" }, { "attribute": "created_at", "operator": "greater_than", "value": "2024-01-01T00:00:00Z" } ] }}The self key targets the primary resource. Additional keys target relationships.
Default Behavior
Section titled “Default Behavior”Without explicit boolean, filters are combined with AND:
{ "filters": { "self": [ { "attribute": "status", "operator": "equals", "value": "active" }, { "attribute": "type", "operator": "equals", "value": "premium" } ] }}// SQL: WHERE status = 'active' AND type = 'premium'Boolean Logic
Section titled “Boolean Logic”AND Conditions
Section titled “AND Conditions”Explicit AND (same as default):
{ "filters": { "self": [ { "attribute": "status", "operator": "equals", "value": "active", "boolean": "and" }, { "attribute": "verified", "operator": "equals", "value": true, "boolean": "and" } ] }}// SQL: WHERE status = 'active' AND verified = trueOR Conditions
Section titled “OR Conditions”{ "filters": { "self": [ { "attribute": "status", "operator": "equals", "value": "pending", "boolean": "or" }, { "attribute": "status", "operator": "equals", "value": "processing", "boolean": "or" } ] }}// SQL: WHERE status = 'pending' OR status = 'processing'Note: For simple OR on same attribute, prefer in operator:
{ "attribute": "status", "operator": "in", "value": ["pending", "processing"] }Mixed Boolean
Section titled “Mixed Boolean”Filters are applied sequentially, with each filter’s boolean determining how it connects to the previous condition:
{ "filters": { "self": [ { "attribute": "type", "operator": "equals", "value": "order" }, { "attribute": "status", "operator": "equals", "value": "pending", "boolean": "or" }, { "attribute": "status", "operator": "equals", "value": "failed", "boolean": "or" } ] }}// SQL: WHERE type = 'order' OR status = 'pending' OR status = 'failed'Important: Boolean operators connect filters left-to-right without automatic grouping. For complex grouping logic, use the in operator:
{ "filters": { "self": [ { "attribute": "type", "operator": "equals", "value": "order" }, { "attribute": "status", "operator": "in", "value": ["pending", "failed"] } ] }}// SQL: WHERE type = 'order' AND status IN ('pending', 'failed')Filtering by Resource
Section titled “Filtering by Resource”Filters can target different resources in a query:
{ "filters": { "self": [ { "attribute": "status", "operator": "equals", "value": "active" } ], "customer": [ { "attribute": "country_code", "operator": "equals", "value": "FI" } ] }}Structure
Section titled “Structure”| Key | Description |
|---|---|
self | Filters on the primary resource |
<relationship> | Filters on related resources |
Relationship Filtering
Section titled “Relationship Filtering”Filtering by relationship creates a WHERE EXISTS condition:
{ "filters": { "self": [ { "attribute": "status", "operator": "equals", "value": "pending" } ], "customer": [ { "attribute": "type", "operator": "equals", "value": "vip" } ] }}// SQL: WHERE status = 'pending'// AND EXISTS (SELECT 1 FROM customers WHERE customers.id = orders.customer_id AND type = 'vip')Allowed Filters
Section titled “Allowed Filters”Servers MUST define which attributes are filterable per resource:
{ "filters": { "self": ["id", "status", "created_at", "total_amount"], "customer": ["id", "type", "country_code"] }}Validation
Section titled “Validation”- Filtering on non-allowed attributes MUST return an error
- Servers SHOULD expose allowed filters via
vend.describe
Type Coercion
Section titled “Type Coercion”String Values
Section titled “String Values”Most operators accept string values:
{ "attribute": "status", "operator": "equals", "value": "pending" }Numeric Values
Section titled “Numeric Values”Comparison operators work with numbers:
{ "attribute": "quantity", "operator": "greater_than", "value": 10 }{ "attribute": "price", "operator": "between", "value": [10.00, 99.99] }Boolean Values
Section titled “Boolean Values”{ "attribute": "is_active", "operator": "equals", "value": true }{ "attribute": "verified", "operator": "equals", "value": false }Date/Time Values
Section titled “Date/Time Values”Use ISO 8601 format:
{ "attribute": "created_at", "operator": "greater_than", "value": "2024-01-15T10:30:00Z" }{ "attribute": "date", "operator": "between", "value": ["2024-01-01", "2024-12-31"] }Null Values
Section titled “Null Values”Use null operators, not equals with null:
// Correct{ "attribute": "deleted_at", "operator": "is_null" }
// Incorrect{ "attribute": "deleted_at", "operator": "equals", "value": null }Sorting
Section titled “Sorting”Sorting allows clients to specify the order of resources in collection responses. Multiple sort criteria are applied in sequence.
Sort Object
Section titled “Sort Object”A sort specifies ordering for a single attribute:
{ "attribute": "created_at", "direction": "desc"}Fields
Section titled “Fields”| Field | Type | Required | Description |
|---|---|---|---|
attribute | string | Yes | Attribute to sort by |
direction | string | Yes | Sort direction |
Direction Values
Section titled “Direction Values”| Value | SQL Equivalent | Description |
|---|---|---|
asc | ASC | Ascending (smallest first) |
desc | DESC | Descending (largest first) |
Sort Arrays
Section titled “Sort Arrays”Multiple sorts create compound ordering:
{ "sorts": [ { "attribute": "status", "direction": "asc" }, { "attribute": "created_at", "direction": "desc" } ]}Order of Application
Section titled “Order of Application”Sorts are applied in array order:
{ "sorts": [ { "attribute": "country_code", "direction": "asc" }, { "attribute": "city", "direction": "asc" }, { "attribute": "name", "direction": "asc" } ]}// SQL: ORDER BY country_code ASC, city ASC, name ASCPrimary sort is first, then secondary, etc.
Allowed Sorts
Section titled “Allowed Sorts”Servers MUST define which attributes are sortable:
{ "sorts": { "self": ["name", "created_at", "updated_at", "status", "total_amount"] }}Validation
Section titled “Validation”- Sorting on non-allowed attributes MUST return an error
- Servers SHOULD expose allowed sorts via
vend.describe
Default Sorting
Section titled “Default Sorting”Server Defaults
Section titled “Server Defaults”When no sort is specified, servers SHOULD apply a default sort:
- By primary key (
id) ascending - By creation timestamp (
created_at) descending - By a natural ordering attribute
Servers MUST document their default sort behavior.
Stable Sorting
Section titled “Stable Sorting”For pagination stability, servers SHOULD include a unique attribute (like id) as the final sort criterion, even if not explicitly requested:
// Client requests{ "sorts": [{ "attribute": "status", "direction": "asc" }] }
// Server applies (for stability)// ORDER BY status ASC, id ASCSort Direction Semantics
Section titled “Sort Direction Semantics”Ascending (asc)
Section titled “Ascending (asc)”| Type | Order |
|---|---|
| Numbers | 1, 2, 3, 10, 100 |
| Strings | A, B, C, a, b, c (locale-dependent) |
| Dates | Oldest first |
| Booleans | false, true |
| Nulls | First or last (implementation-defined) |
Descending (desc)
Section titled “Descending (desc)”| Type | Order |
|---|---|
| Numbers | 100, 10, 3, 2, 1 |
| Strings | c, b, a, C, B, A (locale-dependent) |
| Dates | Newest first |
| Booleans | true, false |
| Nulls | First or last (implementation-defined) |
Null Handling
Section titled “Null Handling”Servers SHOULD document null ordering behavior:
| Behavior | Description |
|---|---|
nulls_first | Nulls sort before non-null values |
nulls_last | Nulls sort after non-null values |
Pagination
Section titled “Pagination”Functions returning lists SHOULD support pagination to handle large result sets efficiently. Vend defines three pagination styles that cover common use cases.
Pagination Object
Section titled “Pagination Object”Pagination parameters are passed in the extension options:
{ "extensions": [ { "urn": "urn:vnd:ext:query", "options": { "pagination": { "limit": 25, "cursor": "eyJpZCI6MTAwfQ" } } } ]}Pagination Styles
Section titled “Pagination Styles”1. Offset-Based
Section titled “1. Offset-Based”Simple pagination using limit and offset. Familiar from SQL.
Request:
{ "pagination": { "limit": 25, "offset": 0 }}Response:
{ "result": { "data": [...], "meta": { "pagination": { "limit": 25, "offset": 0, "total": 150, "has_more": true } } }}Fields
Section titled “Fields”| Field | Type | Description |
|---|---|---|
limit | integer | Maximum items to return |
offset | integer | Number of items to skip |
total | integer | Total items available (optional) |
has_more | boolean | Whether more items exist |
Trade-offs
Section titled “Trade-offs”- Simple to implement and understand
- Allows “jump to page N”
- Performance degrades with large offsets
- Results can shift if data changes between requests
Use when: Small datasets, admin interfaces, or when random page access is needed.
2. Cursor-Based
Section titled “2. Cursor-Based”Opaque cursor token that encodes position. Server generates cursor, client passes it back.
First Request:
{ "pagination": { "limit": 25 }}First Response:
{ "result": { "data": [...], "meta": { "pagination": { "limit": 25, "next_cursor": "eyJpZCI6MTAwLCJkaXIiOiJuZXh0In0", "prev_cursor": null, "has_more": true } } }}Subsequent Request:
{ "pagination": { "limit": 25, "cursor": "eyJpZCI6MTAwLCJkaXIiOiJuZXh0In0" }}Fields
Section titled “Fields”| Field | Type | Description |
|---|---|---|
limit | integer | Maximum items to return |
cursor | string | Opaque cursor from previous response |
next_cursor | string | Cursor for next page (null if none) |
prev_cursor | string | Cursor for previous page (null if none) |
has_more | boolean | Whether more items exist in this direction |
Cursor Encoding
Section titled “Cursor Encoding”Cursors SHOULD be:
- Opaque to clients (implementation detail)
- URL-safe (base64url encoded)
- Tamper-resistant (optionally signed)
Example cursor payload (before encoding):
{ "id": 100, "created_at": "2024-01-15T10:30:00Z", "dir": "next"}Trade-offs
Section titled “Trade-offs”- Efficient for any position (no offset scanning)
- Stable during concurrent modifications
- Cannot jump to arbitrary page
- Cursor may expire or become invalid
Use when: Large datasets, infinite scroll, or when consistency matters.
3. Keyset-Based (Time/ID)
Section titled “3. Keyset-Based (Time/ID)”Pagination using explicit field values. Ideal for feeds and timelines.
Request (ID-based):
{ "pagination": { "limit": 25, "after_id": "msg_abc123", "before_id": null }}Request (Time-based):
{ "pagination": { "limit": 25, "since": "2024-01-15T10:00:00Z", "until": "2024-01-15T12:00:00Z" }}Response:
{ "result": { "data": [...], "meta": { "pagination": { "limit": 25, "newest_id": "msg_xyz789", "oldest_id": "msg_abc124", "has_newer": true, "has_older": true } } }}Fields (Request)
Section titled “Fields (Request)”| Field | Type | Description |
|---|---|---|
limit | integer | Maximum items to return |
after_id | string | Return items after this ID (exclusive) |
before_id | string | Return items before this ID (exclusive) |
since | string | Return items after this timestamp (ISO 8601) |
until | string | Return items before this timestamp (ISO 8601) |
Fields (Response)
Section titled “Fields (Response)”| Field | Type | Description |
|---|---|---|
newest_id | string | ID of newest item in response |
oldest_id | string | ID of oldest item in response |
has_newer | boolean | Whether newer items exist |
has_older | boolean | Whether older items exist |
Combining Parameters
Section titled “Combining Parameters”after_id+limit— Get next N items after IDbefore_id+limit— Get previous N items before IDsince+until— Get items in time rangesince+limit— Get N items since timestamp
Trade-offs
Section titled “Trade-offs”- Perfect for polling (“give me everything since last fetch”)
- Efficient keyset queries
- Requires monotonic IDs or indexed timestamps
- More complex query logic
Use when: Activity feeds, timelines, event logs, or polling scenarios.
Server Implementation
Section titled “Server Implementation”Choosing a Style
Section titled “Choosing a Style”Functions MAY support one or more pagination styles. Servers SHOULD:
- Document which style(s) each function supports
- Return an error if unsupported pagination parameters are provided
- Apply sensible defaults when pagination is omitted
Default Limits
Section titled “Default Limits”Servers SHOULD:
- Define a default
limit(e.g., 25) - Define a maximum
limit(e.g., 100) - Return an error if requested
limitexceeds maximum
Empty Results
Section titled “Empty Results”When no items match:
{ "result": { "data": [], "meta": { "pagination": { "limit": 25, "has_more": false } } }}Client Behavior
Section titled “Client Behavior”Iterating Pages
Section titled “Iterating Pages”Cursor-based example:
cursor = nulldo { response = call("orders.list", { extensions: [{ urn: "urn:vnd:ext:query", options: { pagination: { limit: 25, cursor: cursor } } }] }) process(response.result.data) cursor = response.result.meta.pagination.next_cursor} while (response.result.meta.pagination.has_more)Polling for Updates
Section titled “Polling for Updates”Keyset-based example:
last_id = nullloop { response = call("events.list", { extensions: [{ urn: "urn:vnd:ext:query", options: { pagination: { limit: 100, after_id: last_id } } }] }) if (response.result.data.length > 0) { process(response.result.data) last_id = response.result.meta.pagination.newest_id } sleep(interval)}Sparse Fieldsets
Section titled “Sparse Fieldsets”Sparse fieldsets allow clients to request a subset of attributes for each resource type. This reduces payload size and improves performance by excluding unnecessary data.
Fields Object
Section titled “Fields Object”The fields object specifies which attributes to include per resource type:
{ "fields": { "self": ["id", "status", "total_amount", "created_at"], "customer": ["id", "name"] }}Structure
Section titled “Structure”| Key | Description |
|---|---|
self | Fields for the primary resource |
<relationship> | Fields for related resources |
- Keys are resource type identifiers
- Values are arrays of attribute names
idandtypeare always included (not specified in fieldset)- Empty array
[]returns onlytypeandid - Absent key returns all fields for that type
Request Format
Section titled “Request Format”{ "extensions": [ { "urn": "urn:vnd:ext:query", "options": { "fields": { "self": ["id", "status", "total_amount"], "customer": ["id", "name", "email"] }, "relationships": ["customer"] } } ]}Response Format
Section titled “Response Format”Response includes only requested fields:
{ "result": { "data": { "type": "order", "id": "12345", "attributes": { "status": "pending", "total_amount": { "amount": "99.99", "currency": "USD" } }, "relationships": { "customer": { "data": { "type": "customer", "id": "42" } } } }, "included": [ { "type": "customer", "id": "42", "attributes": { "name": "Alice", "email": "alice@example.com" } } ] }}Note: type and id are always present even when not in the fieldset.
Allowed Fields
Section titled “Allowed Fields”Servers MUST define which fields are queryable per resource:
{ "fields": { "self": ["id", "order_number", "status", "total_amount", "created_at", "updated_at"], "customer": ["id", "name", "email", "type"], "items": ["id", "sku", "name", "quantity", "price"] }}Validation
Section titled “Validation”- Requesting non-allowed fields MUST return an error
- Servers SHOULD expose allowed fields via
vend.describe
Default Behavior
Section titled “Default Behavior”No Fields Specified
Section titled “No Fields Specified”When fields is absent, servers return all allowed fields:
// Request without fields option{ "extensions": [{ "urn": "urn:vnd:ext:query", "options": {} }]}
// Response includes all fields{ "data": { "type": "order", "id": "12345", "attributes": { "order_number": "ORD-2024-001", "status": "pending", "total_amount": { "amount": "99.99", "currency": "USD" }, "item_count": 3, "notes": null, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:35:00Z" } }}Empty Fieldset
Section titled “Empty Fieldset”Empty array returns only type and id:
// Request{ "fields": { "self": [] }}
// Response{ "data": { "type": "order", "id": "12345" }}Field Selection Patterns
Section titled “Field Selection Patterns”Minimal Response
Section titled “Minimal Response”Request only identifiers:
{ "fields": { "self": [] }}Summary View
Section titled “Summary View”Request key fields for list views:
{ "fields": { "self": ["id", "status", "total_amount", "created_at"] }}Detail View
Section titled “Detail View”Request all fields including relationships:
{ "fields": { "self": ["id", "order_number", "status", "total_amount", "notes", "created_at", "updated_at"], "customer": ["id", "name", "email"], "items": ["id", "sku", "name", "quantity", "price"] }, "relationships": ["customer", "items"]}Relationships
Section titled “Relationships”Relationships define connections between resources. Vend supports requesting related resources in a single call, reducing round trips and enabling efficient data fetching.
Requesting Relationships
Section titled “Requesting Relationships”Use the relationships array to include related resources:
{ "extensions": [ { "urn": "urn:vnd:ext:query", "options": { "relationships": ["customer", "items"] } } ]}relationshipsis an array of relationship names- Related resources are returned in the
includedarray - Only declared relationships can be requested
- Order in the array does not affect response
Response Structure
Section titled “Response Structure”Relationship Data
Section titled “Relationship Data”Each relationship contains a data member with resource identifiers:
{ "data": { "type": "order", "id": "12345", "attributes": { ... }, "relationships": { "customer": { "data": { "type": "customer", "id": "42" } }, "items": { "data": [ { "type": "order_item", "id": "1" }, { "type": "order_item", "id": "2" } ] } } }}Relationship Types
Section titled “Relationship Types”| Type | data Value | Description |
|---|---|---|
| To-one | { "type": "...", "id": "..." } | Single related resource |
| To-many | [{ "type": "...", "id": "..." }, ...] | Multiple related resources |
| Empty to-one | null | No related resource |
| Empty to-many | [] | No related resources |
Included Resources
Section titled “Included Resources”When relationships are requested, full resource objects appear in included:
{ "result": { "data": { "type": "order", "id": "12345", "attributes": { "status": "pending" }, "relationships": { "customer": { "data": { "type": "customer", "id": "42" } } } }, "included": [ { "type": "customer", "id": "42", "attributes": { "name": "Alice", "email": "alice@example.com" } } ] }}Compound Documents
Section titled “Compound Documents”A compound document contains the primary resource(s) plus related resources.
Structure
Section titled “Structure”{ "result": { "data": { ... }, // Primary resource(s) "included": [ ... ], // Related resources "meta": { ... } // Optional metadata }}- Each resource in
includedMUST be unique bytype+id - Resources in
includedMUST be referenced by at least one relationship includedresources MAY have their own relationships- Circular references are allowed (resource A → B → A)
Deduplication
Section titled “Deduplication”When multiple resources reference the same related resource, it appears once in included:
{ "data": [ { "type": "order", "id": "1", "relationships": { "customer": { "data": { "type": "customer", "id": "42" } } } }, { "type": "order", "id": "2", "relationships": { "customer": { "data": { "type": "customer", "id": "42" } } } } ], "included": [ { "type": "customer", "id": "42", "attributes": { "name": "Alice" } } // Customer 42 appears only once ]}Nested Relationships
Section titled “Nested Relationships”Request relationships of relationships using dot notation:
{ "relationships": ["customer", "items", "items.product"]}This includes:
customer- The order’s customeritems- The order’s line itemsitems.product- Each item’s product
Response
Section titled “Response”{ "data": { "type": "order", "id": "12345", "relationships": { "customer": { "data": { "type": "customer", "id": "42" } }, "items": { "data": [ { "type": "order_item", "id": "1" }, { "type": "order_item", "id": "2" } ] } } }, "included": [ { "type": "customer", "id": "42", "attributes": { "name": "Alice" } }, { "type": "order_item", "id": "1", "attributes": { "quantity": 2, "price": { "amount": "29.99", "currency": "USD" } }, "relationships": { "product": { "data": { "type": "product", "id": "prod_abc" } } } }, { "type": "order_item", "id": "2", "attributes": { "quantity": 1, "price": { "amount": "49.99", "currency": "USD" } }, "relationships": { "product": { "data": { "type": "product", "id": "prod_xyz" } } } }, { "type": "product", "id": "prod_abc", "attributes": { "name": "Widget", "sku": "WDG-001" } }, { "type": "product", "id": "prod_xyz", "attributes": { "name": "Gadget", "sku": "GDG-002" } } ]}Depth Limits
Section titled “Depth Limits”Servers SHOULD enforce a maximum nesting depth (e.g., 3 levels):
// Allowed"items.product.category"
// May be rejected"items.product.category.parent.parent"Allowed Relationships
Section titled “Allowed Relationships”Servers MUST define which relationships are available:
{ "relationships": ["customer", "items", "shipping_address", "billing_address"]}Validation
Section titled “Validation”- Requesting non-allowed relationships MUST return an error
- Servers SHOULD expose allowed relationships via
vend.describe
Relationship Filtering
Section titled “Relationship Filtering”Filter the primary resource based on related resource attributes:
{ "filters": { "self": [ { "attribute": "status", "operator": "equals", "value": "pending" } ], "customer": [ { "attribute": "type", "operator": "equals", "value": "vip" } ] }, "relationships": ["customer"]}This returns orders that:
- Have status “pending”
- Have a customer with type “vip”
SQL Equivalent
Section titled “SQL Equivalent”SELECT orders.* FROM ordersJOIN customers ON customers.id = orders.customer_idWHERE orders.status = 'pending'AND customers.type = 'vip'Relationship Fields
Section titled “Relationship Fields”Control which fields are returned for related resources:
{ "fields": { "self": ["id", "status", "total_amount"], "customer": ["id", "name"], "items": ["id", "quantity", "price"] }, "relationships": ["customer", "items"]}Without Inclusion
Section titled “Without Inclusion”Request relationship data without full resources:
// Request without relationships option - identifiers only{ "extensions": [{ "urn": "urn:vnd:ext:query", "options": {} }]}
// Response includes relationship identifiers but no included array{ "data": { "type": "order", "id": "12345", "attributes": { ... }, "relationships": { "customer": { "data": { "type": "customer", "id": "42" } } } } // No "included" - only identifiers}This allows clients to:
- See what relationships exist
- Fetch related resources separately if needed
- Reduce payload when relationships aren’t needed
Options (Request)
Section titled “Options (Request)”| Field | Type | Required | Description |
|---|---|---|---|
fields | object | No | Sparse fieldset selection per resource type |
filters | object | No | Filter conditions per resource type |
sorts | array | No | Sort criteria in priority order |
pagination | object | No | Pagination parameters |
relationships | array | No | Relationships to include |
Data (Response)
Section titled “Data (Response)”| Field | Type | Description |
|---|---|---|
capabilities | array | Query capabilities the function supports |
Behavior
Section titled “Behavior”Request Processing
Section titled “Request Processing”When the query extension is included:
- Server MUST validate all query parameters against function schema
- Server MUST apply filters before sorting
- Server MUST apply sorting before pagination
- Server MUST include requested relationships in
includedarray - Server MUST return only requested fields if sparse fieldsets specified
Response Format
Section titled “Response Format”Query responses MUST use resource object format:
{ "result": { "data": { ... }, // Resource object(s) "included": [ ... ], // Related resources (if requested) "meta": { ... } // Query metadata (pagination info, etc.) }}Capability Declaration
Section titled “Capability Declaration”Functions SHOULD declare which query capabilities they support via vend.describe:
{ "result": { "function": "orders.list", "extensions": { "urn:vnd:ext:query": { "capabilities": ["filtering", "sorting", "pagination", "relationships"], "filters": { "self": ["id", "status", "created_at", "total_amount"], "customer": ["id", "type"] }, "sorts": { "self": ["id", "status", "created_at", "total_amount"] }, "relationships": { "available": ["customer", "items"], "nested": { "items": ["product"] }, "max_depth": 3 }, "pagination": { "styles": ["cursor", "offset"], "default_limit": 25, "max_limit": 100 } } } }}Examples
Section titled “Examples”Basic Query Request
Section titled “Basic Query Request”{ "protocol": { "name": "vend", "version": "0.1.0" }, "id": "req_123", "call": { "function": "orders.list", "version": "1", "arguments": {} }, "extensions": [ { "urn": "urn:vnd:ext:query", "options": { "filters": { "self": [ { "attribute": "status", "operator": "in", "value": ["pending", "processing"] } ] }, "sorts": [ { "attribute": "created_at", "direction": "desc" } ], "pagination": { "limit": 25 } } } ]}Query Response
Section titled “Query Response”{ "protocol": { "name": "vend", "version": "0.1.0" }, "id": "req_123", "result": { "data": [ { "type": "order", "id": "12345", "attributes": { "status": "pending", "total_amount": { "amount": "99.99", "currency": "USD" }, "created_at": "2024-01-15T10:30:00Z" } }, { "type": "order", "id": "12346", "attributes": { "status": "processing", "total_amount": { "amount": "149.99", "currency": "USD" }, "created_at": "2024-01-15T09:15:00Z" } } ], "meta": { "pagination": { "limit": 25, "next_cursor": "eyJpZCI6MTIzNDZ9", "has_more": true } } }, "extensions": [ { "urn": "urn:vnd:ext:query", "data": { "capabilities": ["filtering", "sorting", "pagination", "relationships"] } } ]}With Relationships
Section titled “With Relationships”{ "protocol": { "name": "vend", "version": "0.1.0" }, "id": "req_124", "call": { "function": "orders.get", "version": "1", "arguments": { "id": "12345" } }, "extensions": [ { "urn": "urn:vnd:ext:query", "options": { "fields": { "self": ["id", "status", "total_amount"], "customer": ["id", "name", "email"] }, "relationships": ["customer", "items"] } } ]}Relationship Response
Section titled “Relationship Response”{ "protocol": { "name": "vend", "version": "0.1.0" }, "id": "req_124", "result": { "data": { "type": "order", "id": "12345", "attributes": { "status": "pending", "total_amount": { "amount": "99.99", "currency": "USD" } }, "relationships": { "customer": { "data": { "type": "customer", "id": "42" } }, "items": { "data": [ { "type": "order_item", "id": "1" }, { "type": "order_item", "id": "2" } ] } } }, "included": [ { "type": "customer", "id": "42", "attributes": { "name": "Alice", "email": "alice@example.com" } }, { "type": "order_item", "id": "1", "attributes": { "quantity": 2, "price": { "amount": "29.99", "currency": "USD" } } }, { "type": "order_item", "id": "2", "attributes": { "quantity": 1, "price": { "amount": "39.99", "currency": "USD" } } } ] }, "extensions": [ { "urn": "urn:vnd:ext:query", "data": { "capabilities": ["filtering", "sorting", "pagination", "relationships", "sparse_fieldsets"] } } ]}Complex Query
Section titled “Complex Query”{ "protocol": { "name": "vend", "version": "0.1.0" }, "id": "req_complex", "call": { "function": "tracking_events.list", "version": "1", "arguments": {} }, "extensions": [ { "urn": "urn:vnd:ext:query", "options": { "fields": { "self": ["id", "status", "location", "occurred_at"], "shipment": ["id", "tracking_number"] }, "filters": { "self": [ { "attribute": "occurred_at", "operator": "greater_than", "value": "2024-01-01T00:00:00Z" }, { "attribute": "status", "operator": "in", "value": ["in_transit", "delivered"] } ], "shipment": [ { "attribute": "carrier", "operator": "equals", "value": "posti" } ] }, "sorts": [ { "attribute": "occurred_at", "direction": "desc" } ], "relationships": ["shipment"], "pagination": { "limit": 50 } } } ]}Error Handling
Section titled “Error Handling”Invalid Filter Attribute
Section titled “Invalid Filter Attribute”{ "errors": [{ "code": "INVALID_ARGUMENTS", "message": "Filter attribute not allowed: secret_field", "retryable": false, "source": { "pointer": "/extensions/0/options/filters/self/0/attribute" }, "details": { "attribute": "secret_field", "allowed": ["id", "status", "created_at"] } }]}Invalid Sort Attribute
Section titled “Invalid Sort Attribute”{ "errors": [{ "code": "INVALID_ARGUMENTS", "message": "Sort attribute not allowed: internal_score", "retryable": false, "source": { "pointer": "/extensions/0/options/sorts/0/attribute" } }]}Relationship Not Available
Section titled “Relationship Not Available”{ "errors": [{ "code": "INVALID_ARGUMENTS", "message": "Relationship not available: secret_notes", "retryable": false, "source": { "pointer": "/extensions/0/options/relationships/0" }, "details": { "relationship": "secret_notes", "available": ["customer", "items"] } }]}Pagination Limit Exceeded
Section titled “Pagination Limit Exceeded”{ "errors": [{ "code": "INVALID_ARGUMENTS", "message": "Pagination limit exceeds maximum", "retryable": false, "source": { "pointer": "/extensions/0/options/pagination/limit" }, "details": { "requested": 500, "max_limit": 100 } }]}Field Not Allowed
Section titled “Field Not Allowed”{ "errors": [{ "code": "INVALID_ARGUMENTS", "message": "Field not allowed: secret_notes", "retryable": false, "source": { "pointer": "/extensions/0/options/fields/self/3" }, "details": { "field": "secret_notes", "resource": "self", "allowed": ["id", "order_number", "status", "total_amount", "created_at", "updated_at"] } }]}Migration from Arguments
Section titled “Migration from Arguments”If previously using query parameters in call.arguments, migrate to the query extension:
Before (arguments):
{ "call": { "function": "orders.list", "version": "1", "arguments": { "filters": { ... }, "sorts": [ ... ], "pagination": { ... } } }}After (extension):
{ "call": { "function": "orders.list", "version": "1", "arguments": {} }, "extensions": [ { "urn": "urn:vnd:ext:query", "options": { "filters": { ... }, "sorts": [ ... ], "pagination": { ... } } } ]}This separation clarifies that query capabilities are optional protocol features, not function-specific arguments.
Discovery
Section titled “Discovery”Functions SHOULD advertise query capabilities via vend.describe:
{ "result": { "function": "orders.list", "extensions": { "urn:vnd:ext:query": { "capabilities": ["filtering", "sorting", "pagination", "relationships", "sparse_fieldsets"], "filters": { "self": ["id", "status", "created_at", "total_amount"], "customer": ["id", "type", "country_code"] }, "sorts": { "self": ["id", "order_number", "status", "total_amount", "created_at", "updated_at"], "default": [{ "attribute": "created_at", "direction": "desc" }] }, "fields": { "self": ["id", "order_number", "status", "total_amount", "item_count", "notes", "created_at", "updated_at"], "customer": ["id", "name", "email", "type", "country_code"], "items": ["id", "sku", "name", "quantity", "price", "total"], "defaults": { "self": ["id", "order_number", "status", "total_amount", "created_at"] } }, "relationships": { "available": ["customer", "items", "shipping_address", "billing_address"], "nested": { "items": ["product", "product.category"] }, "max_depth": 3 }, "pagination": { "styles": ["cursor", "offset"], "default_limit": 25, "max_limit": 100 } } } }}