Skip to main content
View Markdown

Making API Requests

The ZeyOS client generates methods for every REST operation defined in the ZeyOS OpenAPI specification. These methods provide a clean, high-level interface for all CRUD operations, filtering, sorting, and pagination.

Generated Methods​

All standard API operations are available as client.api.<operationId>(input, options?). The operationId corresponds directly to the operation name in the ZeyOS OpenAPI specification.

JavaScript
// Examples of generated methods
client.api.listTickets(...)
client.api.getTicket(...)
client.api.createTicket(...)
client.api.updateTicket(...)
client.api.deleteTicket(...)
client.api.listAccounts(...)
client.api.createTask(...)

Each method accepts an input object where you can mix path parameters, query parameters, and request body fields in a single flat object. The client automatically routes each property to the correct location based on the operation's parameter definitions.

The examples below assume a client created with validate: true, so mistakes are caught before a request is sent:

JavaScript
const client = createZeyosClient({ platform: 'live', instance: 'demo', validate: true });

CRUD Operations​

List Records​

Retrieve collections of records with optional filters, sorting, and pagination:

JavaScript
const tickets = await client.api.listTickets({
fields: ['ID', 'name', 'status', 'priority', 'duedate'],
filters: { status: 1, visibility: 0 },
sort: ['-lastmodified'],
limit: 50,
});

Get a Single Record​

Fetch a specific record by ID. Additional flags control which related data is included:

JavaScript
const ticket = await client.api.getTicket({
ID: 42,
extdata: 1,
tags: 1,
});

Create a Record​

Create a new record by passing the required fields. For operations without path parameters (like create), you can pass all fields as a flat object:

JavaScript
const newTicket = await client.api.createTicket({
name: 'Fix login bug',
status: 0,
priority: 3,
description: 'Users cannot log in with SSO',
visibility: 0,
});
Required fields the spec does not declare

Some columns are NOT NULL with no database default, so a create that omits them fails server-side with an opaque HTTP 500 — even though the OpenAPI spec marks nothing as required. Most notably, creating an account requires a currency (e.g. "EUR"). client.schema.validate('createAccount', …) now flags this before you send it.

JavaScript
await client.api.createAccount({ lastname: 'Acme Corp', type: 1, currency: 'EUR' });

Update a Record​

Update an existing record with a PATCH request. Pass the ID and changed fields in one object:

JavaScript
await client.api.updateTicket({
ID: 42,
status: 4,
priority: 4,
});
Explicit body is also supported

Use body or data when you want to separate URL parameters from payload fields manually:

JavaScript
await client.api.updateTicket({ ID: 42, body: { status: 4, priority: 4 } });

The PATCH response body contains the full updated record. Use it to confirm the server applied your changes:

JavaScript
const updated = await client.api.updateTicket({
ID: 42,
status: 4,
});
console.log(updated.status); // 4 -- confirmed by the server

Delete a Record​

Delete a record by ID:

JavaScript
await client.api.deleteTicket({ ID: 42 });

Check Existence​

Use a HEAD request to check whether a record exists without downloading the full response body:

JavaScript
const exists = await client.api.existsTicket({ ID: 42 });
// Returns true if the record exists (2xx/3xx), throws ZeyosApiError on 404

Field Selection​

Control which fields are returned in list responses. This reduces payload size and improves performance.

Array Form​

Pass an array of field names to return only those fields:

JavaScript
const result = await client.api.listAccounts({
fields: ['ID', 'lastname', 'contact.city'],
filters: { visibility: 0 },
});

Object Form (with Aliases)​

Pass an object to rename fields in the response. Keys become the output names, values are the source field paths:

JavaScript
const result = await client.api.listAccounts({
fields: {
'Id': 'ID',
'Name': 'lastname',
'City': 'contact.city',
'Agent': 'assigneduser.name',
},
filters: { visibility: 0 },
});
note

Dot-notation field paths (e.g. contact.city, assigneduser.name) allow you to select fields from related or nested objects.

Filtering​

Pass filters as filters (plural). It covers every field type — scalars, enums and GIN-indexed foreign keys:

JavaScript
// Standard filtering -- works for all field types
const active = await client.api.listTickets({
filters: { status: 4, visibility: 0 },
});

// Filter by foreign key field (e.g. project, account)
const projectTickets = await client.api.listTickets({
filters: { visibility: 0, project: projectId },
});

Why not filter (singular)?​

The bundled OpenAPI spec names the parameter filter, but ZeyOS's published API documentation uses filters in every worked example and never mentions the singular. There is one parameter; the spec is the outlier.

Schema validation reflects that: client.schema.validate() and the CLI both reject filter and point you at filters.

note

Validation is off by default in the JS client — pass validate: true to createZeyosClient() (the CLI enables it for you). Without it, a filter key is forwarded to the server as-is.

Use the query parameter to search across a resource's indexed text fields:

JavaScript
const results = await client.api.listAccounts({
fields: ['ID', 'lastname', 'contact.email'],
filters: { visibility: 0 },
query: 'acme',
limit: 20,
});

Distinct Results​

Pass distinct: true to deduplicate result rows. This is useful when using dot-notation joins that may produce multiple rows per record:

JavaScript
const result = await client.api.listAccounts({
distinct: true,
fields: ['ID', 'lastname', 'contact.country'],
filters: { visibility: 0 },
});

Sorting​

Pass an array of field names prefixed with + (ascending) or - (descending):

JavaScript
// Sort by last modified, newest first
const tickets = await client.api.listTickets({
sort: ['-lastmodified'],
});

// Multi-field sort: priority descending, then name ascending
const tickets = await client.api.listTickets({
sort: ['-priority', '+name'],
});

Pagination​

Use limit and offset to page through large result sets:

JavaScript
// Get total count first
const countResult = await client.api.listTickets({
count: true,
filters: { status: 1, visibility: 0 },
});
// countResult contains the total number of matching records

// Fetch the first page
const page1 = await client.api.listTickets({
limit: 50,
offset: 0,
filters: { status: 1, visibility: 0 },
sort: ['+ID'],
});

// Fetch the second page
const page2 = await client.api.listTickets({
limit: 50,
offset: 50,
filters: { status: 1, visibility: 0 },
sort: ['+ID'],
});

Always set an explicit sort when paging. Without one, the API does not promise that rows keep their order between requests.

Offsets shift when data changes

offset counts rows at the moment of each request, and the API has no server-side cursor. If a record is added, removed or moved between two pages, a later page can skip or repeat rows. For jobs that must see every change, use keyset pagination with a checkpoint.

tip

Use count: true to get the total number of matching records without fetching the full dataset. This is useful for building pagination controls.

Auto-pagination​

To iterate an entire result set without managing offset yourself, use client.paginate(operationId, input, opts) — an async iterator that pages until a short/empty page (or opts.max) is reached. It removes the off-by-one and "I only got the first 1000 / 50 rows" mistakes the list caps otherwise invite.

JavaScript
// Stream every matching ticket, one page at a time
for await (const ticket of client.paginate('listTickets', { filters: { visibility: 0 } }, { pageSize: 1000 })) {
process(ticket);
}

// Eagerly collect up to a cap
const recent = await client.collect(
'listTickets',
{ filters: { visibility: 0 }, sort: ['-lastmodified'] },
{ pageSize: 500, max: 2000 }
);

opts: pageSize (default 1000, clamped to the server max of 10000), max (stop after N records), and requestOptions (forwarded to each underlying call, e.g. { signal, timeoutMs }). collect is the eager array form of paginate.

paginate pages by offset, so the caution above applies to it: use it for data that is stable during the read. It keeps no position, so a failed iteration restarts from the beginning.

Normalising List Responses​

List endpoints are not uniform across the full surface area. Depending on the endpoint and response mode, you may see:

  • a plain array
  • an object wrapper with data
  • count metadata alongside the payload when count: true is used

The normalization helpers are useful when you want call sites to share one response-shape contract:

JavaScript
import { normalizeCountResult, normalizeListResult } from '@zeyos/client';

// Without count -- result is a plain array
const raw = await client.api.listTickets({ filters: { visibility: 0 } });
const { data } = normalizeListResult(raw);
// data is always an array

// With count metadata -- result may include both data and count
const raw2 = await client.api.listTickets({ filters: { visibility: 0 }, count: true });
const { data: tickets, count } = normalizeListResult(raw2);
// tickets: array, count: number

// Count-only request -- result may be a number or an object with count metadata
const countOnly = await client.api.listTickets({ filters: { visibility: 0 }, count: true });
const total = normalizeCountResult(countOnly);

Extended Data​

Many ZeyOS entities support extended data (extdata) -- custom fields defined through the platform's extensibility features. By default, extended data fields are not included in API responses. To include them, pass extdata: 1 as a parameter.

Including Extended Data in List Requests​

For list operations, extdata is sent as a body parameter:

JavaScript
const tickets = await client.api.listTickets({
fields: ['ID', 'name', 'status', 'priority', 'duedate'],
filters: { status: 1, visibility: 0 },
sort: ['-lastmodified'],
limit: 50,
extdata: 1,
});

Including Extended Data in GET Requests​

For single-record GET operations, extdata is sent as a query parameter:

JavaScript
const ticket = await client.api.getTicket({
ID: 42,
extdata: 1,
tags: 1,
});

Selecting Specific Extended Data Fields​

You can reference individual extended data fields using dot notation in the fields parameter. Use the extdata.fieldname syntax to select only the custom fields you need:

JavaScript
const tickets = await client.api.listTickets({
fields: {
'Id': 'ID',
'Name': 'name',
'Region': 'extdata.region',
'CustomerType': 'extdata.customer_type',
},
filters: { status: 1, visibility: 0 },
limit: 50,
});
note

When you select specific extdata.* fields via the fields parameter, you do not need to pass extdata: 1 separately -- the selected fields will be included automatically.

Expanding JSON and Binary Columns​

The expand parameter is used to inline the contents of JSON columns or binary/file columns that are normally returned as references or omitted for performance reasons. This applies to structured data columns such as binfile on messages, items on transactions, or data on objects.

JavaScript
// Expand the binary file content of a message
const message = await client.api.getMessage({
ID: 123,
expand: ['binfile'],
});

// Expand the items array on a transaction
const transaction = await client.api.getTransaction({
ID: 456,
expand: ['items'],
});
caution

Do not confuse expand with extdata. The expand parameter is strictly for JSON and binary columns -- it does not apply to extended data fields. To include extended data, use extdata: 1 instead.

Schema Introspection and Validation​

The client.schema surface lets you (or an AI agent) discover the data model and validate inputs without any network call -- it is built from the generated schema. This is the fastest way to learn fields, types, foreign keys, and enum values before issuing a request.

JavaScript
client.schema.resources(); // all resource names
client.schema.describe('tickets'); // { name, type, fields: { status: { type, enum }, account: { fk }, … } }
client.schema.fields('accounts'); // ['ID', 'lastname', 'firstname', 'type', …]
client.schema.operations('tickets'); // ['listTickets', 'getTicket', 'createTicket', …]
client.schema.resourceForOperation('listDunningNotices'); // 'dunning'
client.schema.suggestOperation('listDunning'); // 'listDunningNotices'

Validating a Call​

client.schema.validate(operationId, input) returns structured, self-correcting hints. It returns { valid, errors } without a network call. Each dot-notation hop must be a documented foreign key and the terminal field must exist. This covers selected fields (including alias maps), nested filters, and sorting. Custom keys under extdata require an explicit field contract:

JavaScript
const result = client.schema.validate('createAccount', { name: 'Acme', currency: 'EUR' });
// {
// valid: false,
// errors: [{ field: 'name', message: 'Unknown field "name". Did you mean "lastname"?', suggestion: 'lastname' }]
// }

It flags unknown fields (with a suggestion), filter used where filters is preferred, invalid enum values (listing the valid set), and missing required create fields. The ZeyOS spec carries no required-field metadata, so a curated supplement covers known NOT-NULL-without-default columns — notably accounts require currency:

JavaScript
client.schema.validate('createAccount', { lastname: 'Acme' });
// { valid: false, errors: [{ field: 'currency', message: 'Missing required field "currency" for accounts …', suggestion: 'currency' }] }

Pre-flight Validation​

Set validate: true on the client to validate generated API methods and known-operation client.request() calls before they are sent and throw a ZeyosValidationError (rather than letting the server reject it):

JavaScript
import { ZeyosValidationError } from '@zeyos/client';

const client = createZeyosClient({ platform: 'live', instance: 'demo', validate: true });

try {
await client.api.createAccount({ name: 'Acme' }); // wrong field
} catch (err) {
if (err instanceof ZeyosValidationError) {
console.log(err.operationId); // 'createAccount'
console.log(err.errors); // structured hints (see above)
}
}

Validation is off by default in JavaScript and on by default in CLI/MCP data commands. Enable it in all new code. You can also enable it for one call by passing { validate: true } as a request option.

A raw client.request({ method, path }) without an operation ID has no schema mapping, so even validate: true does not check it. You lose the checks for unknown fields, filter versus filters, enum values, join paths, custom-field keys and required create fields. The first sign of a mistake is then a server error or a silently wrong result. Prefer client.request({ service, operationId, body }), which validates like a generated method.

Custom-field contracts​

Supply the complete reviewed key allowlist for each relevant API resource, using canonical resource names (for example transactions, not a CLI invoice alias):

JavaScript
const client = createZeyosClient({
auth: { mode: 'none' }, // Local validation only; configure auth for API access.
validate: true,
customFields: {
accounts: { region: { type: 'string', enum: ['EMEA', 'APAC'] } },
contacts: { preferred: { type: 'boolean' } },
tickets: {},
},
});

client.schema.validate('listAccounts', {
fields: ['ID', 'contact.city', 'contact.extdata.preferred'],
filters: { 'extdata.region': 'EMEA' },
}); // valid: true for this supplied contract

client.schema.validate('listAccounts', { fields: ['contact.invented'] });
// valid: false; unknown terminal field on contacts
client.schema.validate('listAccounts', { fields: ['extdata.invented'] });
// valid: false; unknown custom field
client.schema.validate('updateAccount', { id: 42, extdata: { region: 'INVALID' } });
// valid: false; value outside the configured enum (no request sent)

A definition {} validates only the key. Optional type supports JSON string, number, integer, boolean, object, and array; optional enum is an array of finite JSON scalar values, and description is a string. Constraints are explicit application contracts, not automatic conversions from the platform's display types. Equality and membership filter operands and write values use these constraints; ordering operands check type without requiring enum membership. This is not a complete filter-operator parser.

A missing resource map returns custom_fields_unavailable; an empty map means the resource is known to have no allowed custom keys. Unknown keys return unknown_custom_field. A key containing dots is matched literally after extdata.; it is not interpreted as a nested JSON property. extdata: 1 on reads still requests the collection without naming individual keys. Writes use an extdata object; a known key set to null deletes that value on update. Joined query paths are not writable fields: update the related resource explicitly.

client.schema.describe(resource).customFields returns the supplied definitions, or null when unavailable. Metadata is copied at client creation; recreate the client after refreshing it. The library does not fetch tenant definitions or verify that your supplied contract belongs to the authenticated instance. CLI/MCP provide an instance-bound snapshot loader.

Binding a Contract to Its Instance​

A contract reviewed for one instance can wrongly pass or reject keys on another, so keep its origin next to it. The CLI and MCP do this for you: they refuse a snapshot from a different instance, and describe reports customFieldSource with the snapshot's platform and live: false. In JavaScript, client.schema.describe() has no customFieldSource, and the snapshot loader is not exported, so your application makes the same check. The snapshot format lets one file serve both:

JavaScript
import { readFile } from 'node:fs/promises';
import { createZeyosClient } from '@zeyos/client';

const normalize = (url) => url.replace(/\/+$/, '');

export async function createBoundClient({ platform, snapshotPath, auth }) {
const snapshot = JSON.parse(await readFile(snapshotPath, 'utf8'));
if (normalize(snapshot.platform) !== normalize(platform)) {
throw new Error('Custom-field snapshot belongs to a different ZeyOS instance.');
}

const client = createZeyosClient({
platform,
auth,
validate: true,
customFields: snapshot.customFields,
});

// Same shape as the CLI: an offline snapshot, not a live reading of the tenant.
return { client, customFieldSource: { platform: normalize(snapshot.platform), live: false } };
}

Log customFieldSource at startup so a run shows which contract it used. It proves where the snapshot was meant to apply, not that it is current. Keep your review date and source separately from the snapshot, because the CLI and MCP loader rejects any extra top-level property in that file. Refresh it after configuration changes.

To discover configured extdata definitions, query customfields using context for the owning entity, source: 0 for EXTDATA, and activity: 0 for active definitions. reference is the custom key. The entity column is the target of an entity-valued display field, not the owner. Page through every result and verify access/completeness; do not turn an incomplete or denied response into an empty allowlist. Retrieve type and options when reviewing value constraints, but do not infer JSON types or enums from labels alone. Record the instance, review date and provenance separately from credentials.

Coverage and migration​

The validator now rejects join suffixes and custom keys that older clients accepted unchecked. Existing validated integrations using extdata.* must supply the reviewed contract before upgrading. CLI/MCP users set ZEYOS_CUSTOM_FIELDS_FILE; do not suppress errors with --no-validate merely to make a request run.

Validation checks known metadata, not server permissions, record existence, all base-field types, every required parameter, join cardinality, arbitrary SQL, or native iXML/Zymba syntax. The curated create requirement remains account currency; the OpenAPI source does not supply a complete required-field contract. group and having return unsupported-validation errors rather than a silent pass. Operations with no bundled resource schema cannot receive field-level checks. A passing preflight still needs a bounded sandbox request for runtime behavior.

Retries and Rate Limiting​

The client retries only specific failures, and whether a failure qualifies depends on the operation. The rule that matters: a write is never repeated unless the server said it was not processed, or you opted in.

What Is Retried​

FailureRead operationWrite operation
429 Too Many RequestsRetriedRetried, because 429 means the request was not processed
503 Service UnavailableRetriedNot retried: a 503 can arrive after the write committed
Other statuses, such as 500 or 502Not retried unless added to retryOnNot retried, even if added to retryOn
Network error or timeoutRetriedNot retried
Caller abort through AbortSignalNeverNever

An operation counts as a read when its method is GET or HEAD, or when its operation ID starts with list, count, search, exists or get. Everything else counts as a write. That includes a raw client.request({ method: 'POST', path }) call, even if it runs a list query, because there is no operation ID to classify it.

JavaScript
// Customize the policy (these values are the defaults)
const client = createZeyosClient({
platform: 'live',
instance: 'demo',
validate: true,
retry: {
maxRetries: 2, // Retries after the first attempt
retryOn: [429, 503], // Candidate statuses; the table above still applies
baseDelayMs: 300, // Backoff base
maxDelayMs: 10000, // Cap for each wait
},
});

// Disable retries entirely, including 429
const noRetry = createZeyosClient({ platform: 'live', instance: 'demo', retry: false });

Delays​

  • After a status response, the client waits for the Retry-After header if it is present (seconds or an HTTP date). Without one, it uses exponential backoff with jitter: baseDelayMs * 2^attempt, plus a random amount up to baseDelayMs.
  • Every wait is capped at maxDelayMs. A Retry-After longer than the cap is shortened, so a retry can arrive earlier than the server asked. If you must respect long rate-limit windows, raise maxDelayMs or handle the 429 in your own job.
  • After a network error or timeout, the client always uses backoff, because there is no response to read.

Network failures and status retries share one maxRetries budget per request. Once it is spent, the last failure is thrown.

Changing the Network Rule​

retryOnNetworkError overrides the read/write classification. Omit it for automatic behavior, or set true or false for a client or a single request. The request value wins.

JavaScript
await client.api.listTickets({ filters: { visibility: 0 } }, { retryOnNetworkError: false });
await client.api.updateTicket({ ID: 42, status: 4 }, { retryOnNetworkError: true }); // Same value each time

true also enables 503 and other retryOn statuses for that operation, not only network errors. Use it on a write only when repeating the request is harmless. An update that sets fixed values usually is, a create is not.

Request Timeout and Job Budget​

timeoutMs bounds one attempt, and each retry gets a fresh deadline. There is no default in JavaScript: without it, a stalled connection can wait forever, and the network retry never runs.

JavaScript
// Per request
await client.api.listTickets({ filters: { visibility: 0 } }, { timeoutMs: 8000 });

// Or a client-wide default
const client = createZeyosClient({ platform: 'live', instance: 'demo', validate: true, timeoutMs: 8000 });

A timeout rejects with an Error where isTimeout === true, code === 'ETIMEDOUT' and name === 'TimeoutError'.

timeoutMs does not limit a whole call. One read can take (maxRetries + 1) * timeoutMs plus the waits, and a token refresh adds requests of its own. To limit a call or a job, pass an AbortSignal:

JavaScript
const signal = AbortSignal.timeout(60_000); // Budget for everything below

for (const page of pages) {
await client.api.listTickets(page, { signal, timeoutMs: 8000 });
}

Aborting your signal stops the current attempt or backoff wait immediately and is never retried. The error is your signal's abort reason, which has no isTimeout property, so you can tell it apart from a per-attempt timeout. Aborting stops the client from waiting. It does not undo a request the server already received.

Uncertain Write Outcomes​

When a write times out, loses its connection, is aborted or returns a 5xx, you do not know whether it happened. A timed-out create may have succeeded. Retrying blindly can create a duplicate, so check first:

JavaScript
import { ZeyosApiError, ZeyosValidationError, normalizeListResult } from '@zeyos/client';

function outcomeIsUncertain(err) {
if (err instanceof ZeyosValidationError) return false; // Nothing was sent.
if (err instanceof ZeyosApiError) return err.status >= 500;
return true; // Timeout, abort or dropped connection.
}

async function findByExternalRef(externalRef) {
const result = await client.api.listTickets({
fields: ['ID'],
filters: { 'extdata.external_ref': externalRef },
limit: 2,
});
return normalizeListResult(result).data;
}

/** externalRef is your own stable ID, generated once per intended ticket. */
export async function createTicketOnce(externalRef, fields) {
const input = { ...fields, extdata: { external_ref: externalRef } };
try {
return await client.api.createTicket(input);
} catch (err) {
if (!outcomeIsUncertain(err)) throw err;
const matches = await findByExternalRef(externalRef); // If this fails, stay uncertain.
if (matches.length > 1) throw new Error(`Duplicate tickets for ${externalRef}.`);
if (matches.length === 1) return matches[0];
throw err; // Not visible yet. Leave the decision to a later reconcile.
}
}

This example needs external_ref in your customFields contract and on the instance. A request that timed out may still commit after your check, so "not found" is not proof of failure: rerun the check after a delay before creating again. If a resource has no field that can hold such a reference, match on business fields you control, and escalate to a person when the match is ambiguous.

Do not use retries for writes with external effects, such as sending a message. Reading the record back cannot prove what happened outside ZeyOS.

Error Handling​

All API errors are thrown as ZeyosApiError instances. This class extends Error and includes structured information about the failed request. When the server returns an error body with a message, a short snippet of it is folded into err.message (e.g. api.listTickets failed with HTTP 400: unknown filter field: bogus), so the thrown error says why, not just the status code. The full body is always on err.body.

JavaScript
import { ZeyosApiError } from '@zeyos/client';

try {
await client.api.getTicket({ ID: 999 });
} catch (err) {
if (err instanceof ZeyosApiError) {
console.log(err.status); // 404
console.log(err.statusText); // 'Not Found'
console.log(err.operationId); // 'getTicket'
console.log(err.service); // 'api'
console.log(err.method); // 'GET'
console.log(err.url); // Full request URL
console.log(err.body); // Error response body (parsed JSON or text)
}
}

Use these properties for debugging, but keep err.body and err.message out of production logs: they can contain record data from the response. The server-side guide shows a redacted log format.

The ZeyosApiError properties:

JsonValue means a parsed JSON scalar, array, or plain JSON object. Non-JSON error responses are returned as strings.

PropertyTypeDescription
statusnumberHTTP status code (e.g. 404, 401, 500)
statusTextstringHTTP status text (e.g. 'Not Found')
operationIdstringThe operation that failed (e.g. 'getTicket')
servicestringThe service key (e.g. 'api', 'oauth2')
methodstringHTTP method used (e.g. 'GET', 'POST')
urlstringThe full request URL
bodyJsonValueThe parsed JSON response body, plain-text error body, or null
headersRecord<string,string>Response headers as a plain object

Unknown Operations​

Calling an operation that does not exist rejects with a ZeyosApiError that suggests the closest match, instead of an opaque is not a function error -- useful when an operationId differs from the underlying table name:

JavaScript
await client.api.listDunning({});
// ZeyosApiError: Unknown operation 'api.listDunning'. Did you mean 'listDunningNotices'?

Validation Errors​

When the client is created with validate: true, malformed requests reject with a ZeyosValidationError before any network call (see Schema Introspection and Validation). It carries operationId and a structured errors array.

Low-Level Requests​

For endpoints not covered by the generated methods, or when you need full control over the request, use client.request().

By Operation ID​

Reference a known operation by its service and operation ID:

JavaScript
const result = await client.request({
service: 'api',
operationId: 'listTickets',
body: { filters: { status: 1, visibility: 0 }, limit: 10 },
});

By Path and Method​

Specify the HTTP method and path directly for custom or undocumented endpoints. This skips schema validation and read classification, so a POST list sent this way is not retried after a network error, a timeout or a 503:

JavaScript
const result = await client.request({
service: 'api',
method: 'POST',
path: '/tickets/',
body: { filters: { status: 1, visibility: 0 } },
});

Raw Responses​

Pass raw: true to receive the full response envelope instead of just the parsed body:

JavaScript
const response = await client.request({
service: 'api',
operationId: 'listTickets',
body: { limit: 10 },
raw: true,
});

console.log(response.status); // 200
console.log(response.headers); // Response headers
console.log(response.data); // Parsed body

Request Options​

All generated methods and client.request() accept an optional second argument with request-level options:

OptionTypeDescription
signalAbortSignalAn AbortController signal to cancel the request
timeoutMsnumberAbort each attempt after N ms (composes with signal); also settable client-wide as timeoutMs
retryOnNetworkErrorbooleanForce or disable retrying network errors, timeouts and retryOn statuses other than 429 for this call (default: reads only)
validatebooleanPass true to validate this call even if the client has validation off
rawbooleanReturn the full response envelope instead of just the data
authstring | { mode?: string, accessToken?: string, access_token?: string, refreshToken?: string, refresh_token?: string, clientId?: string, client_id?: string, clientSecret?: string, client_secret?: string }Override the authentication mode or credentials for this request
baseUrlstringOverride the base URL for this request
bodyType'json' | 'form'Force a body encoding

Example with an abort controller:

JavaScript
const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

const tickets = await client.api.listTickets(
{ limit: 100 },
{ signal: controller.signal }
);
Persisting refreshed tokens

When using token mode with autoRefresh in a trusted environment, tokens are updated in the token store automatically. If you use a MemoryTokenStore, those refreshed tokens will be lost on page reload. Use the syncTokens pattern to persist them:

JavaScript
async function syncTokens() {
const tokenSet = await client.auth.getTokenSet();
if (tokenSet?.accessToken) {
localStorage.setItem('zeyos_tokens', JSON.stringify(tokenSet));
}
}

// Call after important API operations
const tickets = await client.api.listTickets({ limit: 50 });
await syncTokens();