Only this pageAll pages
Powered by GitBook
1 of 22

REST API v3

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Introduction

API v3 is the latest version of the Xytech Platform REST API. It builds on v2 with one goal in mind: make the API easier to use and harder to get wrong, while keeping it fast under load.

v3 is generally available from release 26.5. It is enabled by default - there is no longer a feature flag to turn on - and it runs alongside v2, so existing v2 integrations keep working unchanged.

Why v3?

The API v3 initiative was guided by three objectives:

  • Embrace standards. Align Xytech's API with widely understood REST conventions for URLs, request bodies, and HTTP status codes, so that developers familiar with other modern APIs feel at home.

  • Apply better defaults. Ship sensible, safe defaults - especially around result size - so that a naive call stays performant and doesn't put the system under unnecessary load.

  • Improve functionality. Add capabilities that integrators have asked for, such as querying setup documents and sending complex queries in a request body.

Area
v2
v3
  • Cleaner URLs. Record keys are path segments (/job_no/123) rather than ?job_no=123, so simple lookups read cleanly and need no encoding. (The JSON fields and query parameters still need URL-encoding in strict clients.)

  • Predictable performance. A required fields projection and a system default page size protect both your integration and the platform from accidental "select everything" calls.

  • - a focused checklist of what changes for an existing v2 integration, with before/after examples.

  • - a task-oriented reference for building a new integration on v3: authentication, addressing records, querying, paging, updating, and error handling.

Conventions used in this guide. Examples use https://<your-server> as a placeholder for your Xytech web address - replace it with your own. The exact base URL for your environment is shown on the /apidocs page (see ).

The database

Required in the URL path

Dropped - the server's database is used automatically (name one via ?database= only on multi-database servers)

List queries

GET only, filters in the URL

GET or POST, with a structured JSON body for complex queries

Setup documents

Return every record

Can be filtered like list documents

Field selection

Optional

Required on every GET (JSON fields parameter)

Result size

Unbounded unless you asked for a page

A safe default page size is always applied

PATCH Content-Type

ops = application/json, doc = application/json-patch+json

Reversed/standardised: ops = application/json-patch+json, doc = application/json

Errors

Almost everything was 400

Correct status + machine-readable code in the body

Discovery

-

Every document's v3 Swagger doc is linked from the /apidocs index

  • Standard, actionable errors. A 404 means not found, a 401 means unauthenticated, and the response body carries a stable code you can branch on.

  • More reach. Setup documents - previously all-or-nothing - can now be filtered, and complex queries that used to produce unwieldy URLs can be sent as a JSON body.

  • Addressing a record

    ?keyName=keyValue in the query string

    /keyName/keyValue as clean path segments

    Records with / in the code

    Fails (e.g. AVL/007)

    Supported via a dedicated /where/ route

    What's new at a glance

    Key benefits

    How to read this guide

    Migrating from API v2 to API v3
    API v3 user guide
    API reference

    Base URL and databases

    Base URL

    All v3 endpoints live under:

    https://<your-server>/api/v3/

    <your-server> is your Xytech web address. The exact base URL for your environment is shown at the top of the /apidocs page.

    The database

    In v3 you don't put a database in the URL. Where v2 required a /database/{name}/ segment, v3 uses the server's configured database automatically - so a call is simply:

    GET https://<your-server>/api/v3/JmJob/job_no/123

    The rare exception: multiple databases

    The only time you name a database is the uncommon case where a single server hosts more than one database. Then add a database query parameter to pick one:

    GET https://<your-server>/api/v3/JmJob/job_no/123?database=OtherDatabase

    On the usual single-database server, leave it off entirely. On a multi-database server, omitting it falls back to the server's default database - which may not be the one you want, so name it explicitly in that case.

    The API version is part of the URL path (/api/v3/). There is no api-version header - you select the version by the path you call. v2 and v3 are available simultaneously, so you can migrate endpoint by endpoint.

    Versioning

    API reference (Swagger)

    Every document has an auto-generated OpenAPI (Swagger) specification describing its exact endpoints, fields, and operations. This is the authoritative, always-up-to-date reference for the documents available in your environment.

    The /apidocs index

    Open the API documentation index in a browser:

    https://<your-server>/ApiDocs

    To view the index for a specific database:

    https://<your-server>/ApiDocs/MyDatabase

    The index lists every document grouped by module, with a filter box to find a document quickly. Each document offers a link to its v2 and its v3 specification - use the [v3] link for the current API.

    Spec URLs

    The v3 specification for a single document is served from:

    https://<your-server>/api/v3/spec/{documentCode}

    Compare this to the v2 spec URL, which includes the database in the path:

    https://<your-server>/api/v2/database/{database}/spec/{documentCode}

    The database segment is absent from the v3 spec URL - consistent with v3 not using a database URL parameter (see ).

    The generated spec documents, per document:

    • Available operations (GET / POST / PUT / PATCH) and their routes.

    • Field names and types for request and response bodies.

    • The supported filter operators for querying (mirrors the ).

    Because the reference is generated from your environment, it always reflects the documents and fields actually configured for your database - start here whenever you need the exact field names for a specific document.

    Using the reference

    Base URL and databases
    Filtering reference

    Querying list documents (GET)

    List documents (e.g. JmJoblist) return multiple records. You filter, sort, page, and select fields using query-string parameters.

    GET https://<your-server>/api/v3/JmJobList?fields={"L":["job_no","job_desc"]}&query={"job_no":{"$range":[10,100]}}&sort=job_no&pagesize=50&page=1

    Parameters

    Parameter
    Purpose
    Example

    fields

    Required. JSON projection of fields to return (see below)

    Every v3 GET must include a fields parameter - omit it and the call fails with 400 XY_MISSING_FIELD ("The 'fields' parameter is mandatory for API v3."). This is a change from v2 and applies to single-record, setup, and list GETs alike.

    fields is a JSON object - a comma-separated list is rejected with 400 XY_BAD_REQUEST. The shape is {"table": ["field1", "field2", {"dropdown": ["key1", "key2"]}]}.

    The table name depends on the document type:

    • List and report documents use the list-table alias "L" (and the response is keyed by "L" too):

    • Single-record and setup documents use the document's primary table name (e.g. jm_job, sch_resource, jm_phase):

    Use the to find the exact table and field names for a document. A nested object (e.g. { "division_no": [...] }) projects selected keys of a dropdown/lookup field.

    Encoding note: if your HTTP client is strict about {, }, ", URL-encode the value of the fields and query parameters. For large or deeply nested filters, prefer .

    query takes a JSON object where each key is a field and each value is either a literal (exact match) or an operator object. Operators are prefixed with $.

    See the for the complete operator list.

    Every list response is paged. If you don't set pagesize, the system default (200) is applied; larger requests are clamped to the cap. Read the total row count from the Pagination-Count response header and iterate with page. See .

    API v3 user guide

    This guide walks through everything you need to build an integration on API v3, in the order you'll typically need it:

    1. Fundamentals and data model - documents, primary/sub-tables, and how they map to REST endpoints.

    2. Methods and response codes - the HTTP verbs the API uses and what each returns.

    3. - obtain and send a bearer token.

    4. - where calls go and how the database is chosen.

    5. - the /keyName/keyValue path syntax, and keyEncoding=base16 for keys with special characters.

    6. - the key sub-section in payloads, and date/time conventions.

    7. - filter, sort, and select fields from the URL.

    8. - send complex queries as a JSON body.

    9. - the full set of operators.

    10. - the default page size and pagination headers.

    11. - filtering reference/lookup lists the same way as list documents.

    12. - POST, PUT, and PATCH.

    13. - the saveArgument header for triggering app-server functions (load template, void, approve, etc.).

    14. - naming and using fields added through document customisation.

    15. - worked cURL examples for the calls above.

    16. - status codes and error-code catalogue.

    17. - using the API efficiently.

    18. - the generated per-document reference.

    Every example below uses https://<your-server> for your Xytech web address - substitute your own. v3 calls don't include a database (see ).

    fields={"L":["job_no"]}

    query

    A JSON filter object (see below)

    query={"status":1}

    sort

    Field(s) to sort by

    sort=job_no

    pagesize

    Rows per page (capped at the system default)

    pagesize=50

    page

    1-based page number

    page=2

    fields is required

    The query parameter

    Paging

    Swagger reference
    Querying with POST
    Filtering reference
    Paging and sorting

    Quick start

    Authentication
    Base URL and databases
    Addressing a document
    Key fields and time formats
    Querying list documents (GET)
    Querying with POST
    Filtering reference
    Paging and sorting
    Querying setup documents
    Creating and updating documents
    Save arguments
    Custom field handling
    Examples
    Error responses
    Performance recommendations
    API reference (Swagger)
    Base URL and databases
    // JmJobList
    { "L": ["job_no", "job_desc"] }
    // A single JmJob, or a JmPhase setup query
    { "jm_job": ["job_no", { "division_no": ["division_no", "division_code"] }] }
    // Exact match
    { "status": 1 }
    
    // Comparison
    { "quantity": { "$gte": 100 } }
    
    // Set membership
    { "status": { "$in": [1, 2, 3] } }
    
    // Combine conditions (implicit AND across keys)
    { "status": 1, "priority": { "$gte": 5 } }
    
    // Text contains-all-words
    { "description": { "$likeAnd": "urgent project" } }
    # 1. Get a token (served by v2 - see Authentication; or use a provider bearer token / Basic auth)
    POST https://<your-server>/api/v2/database/<your-database>/orchestration/auth/login
    Content-Type: application/json
    
    { "username": "myuser", "password": "mypassword" }
    # 2. Read a single job (fields is required on every GET)
    GET https://<your-server>/api/v3/JmJob/job_no/123?fields={"jm_job":["job_no","customer_name"]}
    Authorization: Bearer <access_token>
    # 3. Query a list, filtered and paged (list fields use the "L" alias)
    GET https://<your-server>/api/v3/JmJobList?fields={"L":["job_no","job_desc"]}&query={"job_no":{"$range":[10,100]}}&pagesize=50&page=1
    Authorization: Bearer <access_token>

    Migrating from API v2 to API v3

    v3 runs side by side with v2 - nothing you have today stops working. You migrate by pointing calls at the v3 path and adjusting for the changes below. You can migrate one endpoint at a time.

    Each section states the change, shows a v2 ? v3 before/after, and flags anything that could break a naive port.

    1. Change the base path and drop the database

    - GET https://<your-server>/api/v2/database/MyDatabase/JmJob/...
    + GET https://<your-server>/api/v3/JmJob/...

    v3 no longer takes a database in the URL - the server uses its configured database automatically, so drop the /database/{name}/ segment entirely.

    Watch out: the only exception is a server that hosts more than one database. There, add ?database=Name to target one; otherwise the server falls back to its default. See .

    Because the value is now a path segment, = no longer appears in your URLs.

    A /where/ route captures the rest of the path, slashes included:

    This is the change most likely to break an existing integration. Every v3 GET must include a fields parameter, and it must be JSON (a comma-separated list is rejected):

    Omit it and you get 400 XY_MISSING_FIELD; send a comma list and you get 400 XY_BAD_REQUEST. The shape is {"table": ["field", {"dropdown": ["key1","key2"]}]}. Applies to single-record, setup, and list GETs. See .

    v3 always applies the system default page size (200 unless your administrator changed MAX_LIST_ROWS_REST_API); larger requests are clamped.

    Watch out: if your v2 code assumed one response held all rows, it must now page. Read the total from the Pagination-Count response header and loop with ?page=. See .

    GET with query still works for simple cases. See .

    Setup documents (e.g. TcActivity, JmPhase, MoTask) can now be filtered with the same query and fields parameters as list documents, instead of returning every record. See .

    v3 standardises PATCH bodies on the conventional mapping, which is the opposite of v2:

    Watch out: swap your PATCH Content-Type headers. See .

    v3 returns the correct status and a JSON body { status, code, error }:

    You'll now see
    When

    Watch out: branch on the HTTP status and the body code (not the free-text error, and not a blanket 400). See .

    404

    Document or record does not exist

    500

    Unexpected server error

    Use /where/{key} for codes that can contain /.
  • Body

    v2 Content-Type

    v3 Content-Type

    [{op, path, value}] operations

    application/json

    application/json-patch+json

    Document payload

    application/json-patch+json

    400 (with a specific code)

    Validation / business-logic / bad-JSON errors

    401

    Missing or invalid credentials

    403

    Authenticated but not permitted

    2. Address records with path segments, not key=value

    3. Retrieve records whose code contains /

    4. fields is now REQUIRED on every GET

    5. A default page size is always applied

    6. Send complex queries with POST

    7. Filter setup documents

    8. PATCH content-type mapping is reversed from v2

    9. Handle real HTTP status codes

    Migration checklist

    Base URL and databases
    Querying list documents
    Paging and sorting
    Querying with POST
    Querying setup documents
    Creating and updating documents
    Error responses

    application/json

    - GET https://<your-server>/api/v2/database/MyDatabase/JmJob/job_no=123
    + GET https://<your-server>/api/v3/JmJob/job_no/123
    - (not possible in v2)
    + GET https://<your-server>/api/v3/SchResource/where/resource_code=AVL/007
    - GET .../api/v2/.../JmJob/job_no=123
    + GET .../api/v3/JmJob/job_no/123?fields={"jm_job":["job_no","customer_name"]}
    - GET .../api/v2/.../JmJoblist?query=<long, heavily-encoded filter>
    + POST .../api/v3/JmJoblist/query
    + { "filtering": { ... }, "paging": { ... }, "resultColumns": [ ... ] }
      PATCH https://<your-server>/api/v3/JmJob/job_no/123
    - Content-Type: application/json                 # v2 op array
    + Content-Type: application/json-patch+json       # v3 op array
      [ { "op": "replace", "path": "job_desc", "value": "Barry" } ]
    - HTTP/1.1 400 Bad Request
    + HTTP/1.1 404 Not Found
    + { "status": 404, "code": "XY_NOT_FOUND", "error": "..." }

    Addressing a document

    v3 addresses a specific record using path segments for the key, instead of the key=value query syntax used by v1/v2.

    The standard form: /document/keyName/keyValue

    GET https://<your-server>/api/v3/JmJob/job_no/123
    • JmJob - the document code.

    • job_no - the key field name.

    • 123 - the key value.

    Every v3 GET also requires a JSON fields parameter (omitted from the addressing examples here for clarity) - see Querying list documents.

    Because the value is a path segment rather than a query parameter, the = character no longer appears in your URLs, so most simple lookups need no manual URL-encoding.

    Some documents let users define their own codes, and those codes can contain characters that are not valid in a URL path segment - most commonly a /, for example a resource code of AVL/007. A raw or percent-encoded / in a path segment isn't reliable: IIS request filtering, reverse proxies (ARR), and load balancers commonly decode, normalize, or reject %2F before your request reaches the API.

    v3 solves this by letting the client Base16 (hex) encode the key value and signal it with the keyEncoding=base16 query parameter. The server decodes the value before doing the lookup, so the special character never travels through the URL as itself.

    Here 41564c2f303037 is the Base16 encoding of AVL/007 - every byte of the UTF-8 value written as two hex digits (A?41, V?56, L?4c, /?2f, ...). The hex alphabet (0-9a-f, case-insensitive) contains only URL-unreserved characters, so it passes through every layer of infrastructure unchanged.

    Rules:

    • keyEncoding is explicit, never auto-detected - without it, key values are used verbatim. This is deliberate: a plain value can itself look like valid hex (e.g. face), so guessing would silently corrupt ordinary keys.

    • The only supported value today is keyEncoding=base16.

    Use keyEncoding=base16 whenever a key value may contain special characters. Documents with user-definable codes where this commonly applies include:

    • SchResource (resource codes)

    • SchGroup (group codes)

    • JmBillingCode (billing codes)

    ...and any other document whose code is user-definable.

    Rule of thumb: if the value is a clean, simple identifier, address it directly - /{keyName}/{keyValue}. If it can contain a / or other special character, Base16-encode the value and add ?keyEncoding=base16.

    v2 compatibility note: v2's /where/{key} route (which captured the rest of the path, slashes included) still exists, but only under v2 URLs - /api/v2/database/{database}/{document}/where/{key}. It is not available under v3 URLs: calling /api/v3/.../where/... does not invoke it - where is parsed as an ordinary key name and the call fails with a "key not valid" error. Use keyEncoding=base16 for all v3 calls.

    Querying setup documents

    Setup documents hold reference/lookup data - for example:

    • TcActivity - TimeCard activities

    • JmPhase - phases

    It applies to every key-value path segment in the request - the main {keyName}/{keyValue}, and any subtable or sub-subtable key segments. If any key in the URL needs encoding, Base16-encode all key segments in that request; you can't mix an encoded segment with a plain one under a single keyEncoding flag.
  • Works the same way across GET, PATCH, PUT, and DELETE - anywhere v3 addresses a record via /keyName/keyValue.

  • SysAddress (address codes)

    Keys that contain a slash or other special characters

    - v2:  GET /api/v2/database/MyDatabase/JmJob/job_no=123
    + v3:  GET /api/v3/JmJob/job_no/123
    GET https://<your-server>/api/v3/SchResource/resource_code/41564c2f303037?keyEncoding=base16&fields={"sch_resource":["resource_code","resource_desc"]}
    MoTask - media order tasks

    In earlier versions, requesting a setup document returned every record, leaving your integration to receive and filter the whole set. v3 lets you filter setup documents with the same query syntax as list documents, so you can ask for only what you need.

    Like every v3 GET, a setup query requires a JSON fields parameter (see Querying list documents).

    Everything from the list-query pages applies to setup documents too:

    • Filter operators - see the Filtering reference.

    • Field selection with the JSON fields parameter (required).

    • Paging and sorting, including the default page size.

    • Complex filters via where it helps.

    Tip: setup documents are subject to the same default page size as list documents. If a setup document has more rows than the page size, page through it the same way.

    # All activities (fields required; still paged by the default page size)
    GET https://<your-server>/api/v3/TcActivity?fields={"tc_activity":["activity_code","activity_desc"]}
    
    # Only the activities you care about
    GET https://<your-server>/api/v3/TcActivity?fields={"tc_activity":["activity_code","activity_desc"]}&query={"active":1}
    POST /query

    Error responses

    In v2 most failures came back as 400. v3 returns the correct HTTP status code and a JSON body identifying the error.

    Response shape

    HTTP/1.1 404 Not Found
    Content-Type: application/json
    
    {
      "status": 404,
      "code": "XY_NOT_FOUND",
      "error": "Alternate key not found: table=sch_resource, column=resource_code, value=FRED01"
    }

    The body has three fields:

    • status - the numeric HTTP status (matches the status line).

    • code - a stable, machine-readable error code (the XY_* values below). Branch on this.

    • error - a human-readable message. Log it; don't branch on the text.

    v3 uses standard HTTP status codes; where several client errors map to 400, the code field distinguishes them.

    1. Check the HTTP status class: 2xx success, 4xx client error, 5xx server error.

    2. For 4xx, branch on the body code (e.g. re-authenticate on XY_UNAUTHORIZED, surface the missing field on XY_MISSING_FIELD

    XY_INVALID_JSON

    400

    The request body was not valid JSON, or didn't match the Content-Type's expected shape

    XY_INVALID_OPERATION

    400

    The operation could not be applied in the current state

    XY_BUSINESS_LOGIC_ERROR

    400

    The request broke a business rule (e.g. a required related value is missing)

    XY_UNAUTHORIZED

    401

    Missing, malformed, or invalid/expired credentials

    XY_FORBIDDEN

    403

    Authenticated, but not permitted

    XY_NOT_FOUND

    404

    The document or record does not exist

    XY_INTERNAL_SERVER_ERROR

    500

    An unexpected server-side error

    ).
  • Log error for diagnostics; don't match on its free text.

  • code

    HTTP

    Meaning

    XY_MISSING_FIELD

    400

    A required field/parameter was missing (e.g. the mandatory fields parameter)

    XY_BAD_REQUEST

    400

    Status codes and error codes

    Recommended handling

    The request or a parameter was malformed (e.g. fields not valid JSON)

    Custom field handling

    Custom fields

    Any custom fields you define through document customization are automatically included in the REST API definition. They follow a specific naming convention: the Customization Code you created during document customisation, concatenated with the internal field name by an underscore:

    {customization code}_{field name}

    These custom fields appear in GET responses and can be set in POST/PATCH/PUT payloads just like standard fields, using that concatenated name.

    Custom drop-down fields with additional attributes

    You can enable a document-customisation flag, "Additional API details", on a custom drop-down field. When enabled, a REST API call returns the additional attributes stored with the dropdown record (such as external_key) rather than just the value.

    • Not enabled - the custom drop-down field returns its value only.

    • Enabled - the field returns an object including the additional attributes (e.g. external_key).

    Screenshots of the document-customisation screens (Customization Code, internal field names, and the "Additional API details" checkbox) are available in the existing REST API documentation and apply unchanged to v3.

    Creating and updating documents

    All write bodies are document-shaped JSON: an object keyed by the primary table name whose value is an array of records (the same shape a GET returns, minus the read-only key sub-sections). Times follow ISO-8601 - see Key fields and time formats.

    Create - POST

    POST to the document endpoint with Content-Type: application/json and a document payload.

    POST https://<your-server>/api/v3/SchResource
    Authorization: Bearer <access_token>
    Content-Type: application/json
    
    {
      "sch_resource": [
        { "resource_code": "FRED01", "resource_desc": "Fred", "external_key": "Ext001" }
      ]
    }

    Success is 201 Created, and the body reports the identifiers of the created record(s):

    {
      "message": null,
      "tables": ["sch_resource"],
      "sch_resource": {
        "record_identifiers": ["resource_code"],
        "records": [ { "resource_code": "FRED01" } ]
      }
    }

    To generate a primary key, set it to -1 (see Key fields and time formats).

    Update - PATCH (two body formats)

    PATCH updates an existing record. The body format is selected by Content-Type:

    • op - the operation, e.g. replace, add, remove.

    • path - the field name (no leading slash).

    The primary key must appear both in the URL and in the body.

    Sending an operation array with application/json, or a document payload with application/json-patch+json, is rejected - match the body to the Content-Type.

    PUT creates a record, or updates it if one already exists (matched on external_key). Use the keyed URL with Content-Type: application/json and a document payload.

    Success is 204 No Content. Use DELETE sparingly - changing a record's status is often preferable to removing it.

    List endpoints accept PATCH to update every record matching a query - the API equivalent of the UI's Grid Update. Send the operation array with application/json-patch+json; see .

    Field names and requirements vary per document - the generated is the authoritative source for each document's schema.

    Paging and sorting

    To keep responses fast and protect the platform, v3 always applies a page size to list results:

    • If you don't send a page size, the system default is used.

    • If you request a page size larger than the cap, it is clamped to the cap.

    The default/cap is 200 rows unless your administrator has changed the MAX_LIST_ROWS_REST_API

    Success is typically 204 No Content.

    Content-Type

    Body

    Use for

    application/json-patch+json

    An array of operations [{ "op", "path", "value" }]

    Changing specific fields

    application/json

    A document payload (same shape as POST)

    Field operations - application/json-patch+json

    Document payload - application/json

    Upsert - PUT

    Delete - DELETE

    Bulk update via a list endpoint

    Examples
    Swagger reference

    Document-style updates

    setting. This means a "fetch everything" call is no longer possible in a single request - you page through results instead.
    Where
    Page number
    Page size

    GET query string

    page (1-based)

    pagesize

    POST /query body

    paging.pageNo

    paging.pageSize

    A JSON fields parameter is required on every GET; the paging examples below omit it for brevity - see Querying list documents.

    Paging information comes back as HTTP response headers, not in the body:

    Header
    Meaning

    Pagination-Count

    Total number of rows available across all pages

    Pagination-Page

    The current page number (1-based)

    Pagination-Limit

    The page size applied to this response

    1. Make the first request (optionally with a pagesize).

    2. Read Pagination-Count (total rows) and Pagination-Limit (page size).

    3. Compute the number of pages: ceil(Pagination-Count / Pagination-Limit).

    4. Request each subsequent page with page=2, page=3, . until you've read them all.

    • GET: sort=job_no

    • POST: "sorting": [ { "field": "job_no", "direction": "asc" } ] - direction is asc (default) or desc.

    A default page size is always applied

    Paging parameters

    Pagination metadata (response headers)

    Paging through a full result set

    Sorting

    PATCH https://<your-server>/api/v3/JmJob/job_no/123
    Authorization: Bearer <access_token>
    Content-Type: application/json-patch+json
    
    [
      { "op": "replace", "path": "job_desc",     "value": "Hello World" },
      { "op": "replace", "path": "external_key", "value": "15411" }
    ]
    PATCH https://<your-server>/api/v3/JmWorkOrder/wo_no_seq/8248-1
    Authorization: Bearer <access_token>
    Content-Type: application/json
    
    {
      "jm_work_order": [
        {
          "wo_no_seq": { "wo_no_seq": "8248-1" },
          "wo_desc": "My New Description",
          "po": "123456"
        }
      ]
    }
    PUT https://<your-server>/api/v3/SchResource/resource_code/FRED01
    Authorization: Bearer <access_token>
    Content-Type: application/json
    
    {
      "sch_resource": [
        { "resource_code": "FRED01", "resource_desc": "Fred (updated)", "external_key": "Ext001" }
      ]
    }
    DELETE https://<your-server>/api/v3/SchResource/resource_code/FRED01
    GET https://<your-server>/api/v3/JmJobList?fields={"L":["job_no"]}&pagesize=50&page=2
    # Page 1
    GET https://<your-server>/api/v3/JmJoblist?pagesize=100&page=1
    # -> Pagination-Count: 250, Pagination-Page: 1, Pagination-Limit: 100
    
    # Page 2
    GET https://<your-server>/api/v3/JmJoblist?pagesize=100&page=2
    
    # Page 3 (final; 50 rows)
    GET https://<your-server>/api/v3/JmJoblist?pagesize=100&page=3

    Known issues

    Issues currently known to affect API v3. Each entry describes the behaviour you will see, and the recommended approach until it is addressed.

    Fixed in 26.5HF1 / 26.5M. The four POST /query filter defects previously listed here - $-prefixed operators being silently ignored, a second condition on the same field replacing the first, an "or" group combining its conditions with AND instead of OR, and a second sibling "or" group discarding the first - are corrected as of 26.5HF1 and 26.5M. See and for the current syntax. On a build earlier than 26.5HF1 / 26.5M, all four defects are still present.

    Filter groups: nesting an or group inside another or group is rejected

    An "or" group cannot contain another "or" group - the request is rejected with an error naming the field rather than the filter being silently altered. This holds up to two levels of alternation; see Querying with POST for the shapes this allows and disallows.

    The same limitation applies to the $or filter in API v2.

    Filtering reference
    Querying with POST

    Examples

    Basic v3 calls. Replace <your-server> with your Xytech web address and <access_token> with your bearer token (see Authentication). Every GET must include a JSON fields parameter.

    GET a single record

    curl --location --globoff \
      'https://<your-server>/api/v3/SchResource/resource_code/FRED01?fields={"sch_resource":["resource_code","resource_desc","external_key"]}&nullvaluehandling=ignore' \
      --header 'Authorization: Bearer <access_token>'

    Response - note the key sub-section (resource_code) plus root fields:

    {
      "sch_resource": [
        {
          "resource_code": { "resource_code": "FRED01" },
          "resource_desc": "Fred",
          "external_key": "Ext001"
        }
      ]
    }

    GET a filtered list

    List documents project fields under the list-table alias "L" (see Querying list documents):

    curl --location --globoff \
      'https://<your-server>/api/v3/JmJobList?fields={"L":["job_no","job_desc"]}&query={"job_no":{"$range":[10,100]}}&pagesize=50&page=1' \
      --header 'Authorization: Bearer <access_token>'

    GET a record whose key contains a slash

    Base16 (hex) encode the key value and add keyEncoding=base16. AVL/007 encodes to 41564c2f303037 - see Addressing a document for the full rule set.

    Response (201 Created):

    An array of {op, path, value} operations; path is the bare field name.

    Response: 204 No Content.

    Update every record matching a query - the API equivalent of the UI's Grid Update. Send the operation array with application/json-patch+json:

    For a text "contains" match use $likeAnd, e.g. query={"job_desc":{"$likeAnd":"Sport"}}. See the Filtering reference.

    Create, or update if a record with the same external_key exists. Keyed URL, application/json, document payload.

    Response: 204 No Content.

    A ready-to-run collection of these v3 examples is published publicly on Postman:

    Open it in the Postman web app or Run in Postman to import it into your own workspace, then set the collection variables (baseUrl, username, password) and send the requests. The collection groups the calls into Authentication, Read, Query & filter, Create & update, and Save arguments, and mirrors the rules on these pages - fields is required on every GET, list documents use the "L" alias, and v3 data calls take no database in the URL.

    Querying with POST

    Complex queries used to force everything into a long, heavily encoded URL. v3 adds a POST /query route on list and report endpoints that accepts a structured JSON body, so filtering, paging, sorting, and field selection all travel in the request body.

    The same route is available for report documents:

    filtering and fields are required; paging and sorting are optional.

    POST - create a record

    PATCH - change specific fields (application/json-patch+json)

    PATCH - document payload (application/json)

    PATCH via a list endpoint (bulk update by query)

    PUT - upsert

    DELETE

    Postman collection

    Xytech REST API v3 - Examples (Postman)
    curl --location --globoff \
      'https://<your-server>/api/v3/SchResource/resource_code/41564c2f303037?keyEncoding=base16&fields={"sch_resource":["resource_code","resource_desc"]}' \
      --header 'Authorization: Bearer <access_token>'
    curl --location 'https://<your-server>/api/v3/SchResource' \
      --header 'Content-Type: application/json' \
      --header 'Authorization: Bearer <access_token>' \
      --data '{ "sch_resource": [ { "resource_code": "FRED01", "resource_desc": "Fred", "external_key": "Ext001" } ] }'
    {
      "message": null,
      "tables": ["sch_resource"],
      "sch_resource": {
        "record_identifiers": ["resource_code"],
        "records": [ { "resource_code": "FRED01" } ]
      }
    }
    curl --location --request PATCH 'https://<your-server>/api/v3/JmJob/job_no/67981' \
      --header 'Content-Type: application/json-patch+json' \
      --header 'Authorization: Bearer <access_token>' \
      --data '[
        { "op": "replace", "path": "job_desc",     "value": "Hello World" },
        { "op": "replace", "path": "external_key", "value": "15411" }
      ]'
    curl --location --request PATCH 'https://<your-server>/api/v3/JmWorkOrder/wo_no_seq/8248-1' \
      --header 'Content-Type: application/json' \
      --header 'Authorization: Bearer <access_token>' \
      --data '{ "jm_work_order": [ { "wo_no_seq": { "wo_no_seq": "8248-1" }, "wo_desc": "My New Description", "po": "123456" } ] }'
    curl --location --globoff --request PATCH \
      'https://<your-server>/api/v3/JmWorkOrderlist?query={"date_added":{"$range":["2022-07-26T00:00:00","2023-07-28T00:00:00"]},"wo_type_no":40}' \
      --header 'Content-Type: application/json-patch+json' \
      --header 'Authorization: Bearer <access_token>' \
      --data '[ { "op": "replace", "path": "wo_reference", "value": "Quick Service 2" } ]'
    curl --location --request PUT 'https://<your-server>/api/v3/SchResource/resource_code/FRED01' \
      --header 'Content-Type: application/json' \
      --header 'Authorization: Bearer <access_token>' \
      --data '{ "sch_resource": [ { "resource_code": "FRED01", "resource_desc": "Fred (updated)", "external_key": "Ext001" } ] }'
    curl --location --request DELETE 'https://<your-server>/api/v3/SchResource/resource_code/FRED01' \
      --header 'Authorization: Bearer <access_token>'
    Member
    Type
    Required
    Purpose

    filtering

    object

    Yes

    A filter group (see below). A missing filter, or one that resolves to no criteria, is rejected with "A valid filter is missing"

    fields

    array

    Yes

    A filter group combines conditions and can nest further groups, which lets you express AND/OR logic that would be awkward in a URL:

    That reads as status in (1,2) AND (priority >= 8 OR isRush != 0).

    Each condition is { "field", "op", "value" }, where op is one of the operators in the Filtering reference. field is required; op defaults to eq. The value can be a scalar or an array (for in / notin / range).

    Combination is two levels deep:

    A filter is an AND of any number of conditions and any number of OR groups. Each OR group is an OR of branches. Each branch is an AND of conditions.

    Inside an "or" group, each condition is a branch of its own, and each sub-group becomes one branch holding that sub-group's AND-ed conditions.

    Shape
    Supported
    How to write it

    A AND B

    Yes

    Two conditions in one group

    A OR B

    Yes

    Two conditions in a group with combiningOperator: "or"

    Two levels of alternation is the limit. Deeper logic needs the criteria restructured, or two result sets fetched and combined by the caller.

    Conditions naming the same field are combined, not replaced. Combinations that cannot be represented are rejected rather than approximated - the Filtering reference carries the full matrix. In short:

    • gte + lte, or gt + lt, combine into a range

    • ne, notin and contains accumulate - every exclusion or pattern applies

    • Mixing an inclusive and an exclusive bound (gt + lte) is rejected; use range

    • Two equalities are rejected; use in for "either of"

    • GET (?query=...) is convenient for simple, short filters and quick manual checks.

    • POST (/query) is the right choice for complex or nested filters, large value lists, or anything that would make the URL unwieldy.

    Both go through the same query engine and return the same shape of result, including the same pagination headers. The filter syntax differs, though, so a filter cannot be moved between the two unchanged:

    GET ?query=

    POST /query

    Operator spelling

    $-prefixed - {"priority":{"$gte":5}}

    unprefixed op - {"field":"priority","op":"gte","value":5}

    OR logic

    $or

    POST https://<your-server>/api/v3/JmJoblist/query
    Authorization: Bearer <access_token>
    Content-Type: application/json
    
    {
      "filtering": {
        "combiningOperator": "and",
        "conditions": [
          { "field": "status",   "op": "in",  "value": [1, 2, 3] },
          { "field": "priority", "op": "gte", "value": 5 }
        ]
      },
      "paging":  { "pageNo": 1, "pageSize": 50 },
      "sorting": [ { "field": "job_no", "direction": "asc" } ],
      "fields": [
        { "table": "JmJob", "columns": ["job_no", "customerName", "status"] }
      ]
    }
    POST https://<your-server>/api/v3/{document}report/query

    Operator names in a POST body are written without a prefix - in, gte, contains. The GET query parameter requires $-prefixed operators instead. The two spellings are not interchangeable; see the Filtering reference.

    Request body

    {
      "combiningOperator": "and",       // "and" (default) or "or"
      "conditions": [
        { "field": "status", "op": "in", "value": [1, 2] }
      ],
      "groups": [                        // nested groups
        {
          "combiningOperator": "or",
          "conditions": [
            { "field": "priority", "op": "gte", "value": 8 },
            { "field": "isRush",   "op": "ne",  "value": 0 }
          ]
        }
      ]
    }

    resultColumns is still accepted as a deprecated alias for fields, for callers migrating from earlier v3 builds. fields wins when a body sends both.

    Unrecognised members are ignored rather than rejected. A body sending fieldSelection instead of fields loses its column list silently and then fails the required-columns check, which reads as an unrelated error. Check member names against the table above when a request fails for no apparent reason.

    filtering - filter groups

    Filter shapes you can express

    Two conditions on the same field

    GET or POST?

    [ { "table", "columns": [...] } ] - field projection. Optional only when the request sends Return-All-Fields: true

    paging

    object

    No

    { "pageNo", "pageSize" }. Defaults apply when omitted

    sorting

    array

    No

    [ { "field", "direction" } ], direction = asc (default) or desc. Must be an array even for one sort key

    C AND (A OR B)

    Yes

    An AND group with its own condition plus one OR sub-group

    (A OR B) AND (C OR D)

    Yes

    Two OR groups side by side under an AND parent

    (A AND B) OR (C AND D)

    Yes

    An OR group whose branches are AND groups

    A OR B OR C

    Yes

    Also accepted written as nested ORs; they are flattened

    A OR (B AND (C OR D))

    No

    An OR inside a branch of another OR. Rejected with "An 'or' group cannot contain another 'or' group."

    combiningOperator: "or" on a group

    likeAnd matching

    $likeAnd

    not available

    Field projection

    fields parameter

    fields array (deprecated alias: resultColumns)

    Fundamentals and data model

    Xytech is, at a fundamental level, a system that creates, updates, and uses a particular type of data object called a document.

    • Each Xytech document represents a database table (or a collection of tables) and has a REST API endpoint.

      • Each endpoint has a primary table and may have one or more sub-tables.

      • All sub-tables are children of the primary table, and a sub-table can itself have child sub-tables.

    • Each endpoint represents one of the following document types:

      • Setup - generally describes a single item and usually contains only a primary table. Setup documents manage simple items used to build lists of options in other documents (status labels, predefined code sets, etc.). In v3 these can be filtered - see .

      • Maintenance - describes master data (used in transactional data) or transactional data. Maintenance documents often contain one or more sub-tables.

    The REST API is JSON-based. Every endpoint has an OpenAPI 3.0 specification available in your environment - see to browse it.

    A document's payload is a JSON object keyed by the primary table name, whose value is an array of records. Sub-tables appear as nested arrays within a record. For example, a Work Order (jm_work_order) can carry transaction sub-tables inside each record.

    Where a ~/ prefix appears before an endpoint in the API reference, that indicates a sub-table endpoint of the primary endpoint.

    This page covers the shared concepts. For how records are addressed and keyed, see and .

    List - provides access to sets of other records, such as Setup and Maintenance documents. List endpoints support GET and POST queries - see Querying list documents (GET) and Querying with POST.

    Documents, sub-tables, and endpoints

    Querying setup documents
    API reference (Swagger)
    Addressing a document
    Key fields and time formats

    Save arguments

    Some operations require a saveArgument header to tell the app server to perform a function as part of an API call. For example, to load a Work Order Template when creating a Work Order, you supply the saveArgument header and include the wo_template_no in the payload.

    Examples below use v3 conventions: the /api/v3/ base, path-segment keys, and a bearer token. Where a Save Argument is used with a PATCH field-operation body ([{op, path, value}]), it is sent with Content-Type: application/json-patch+json; a POST with a document payload uses Content-Type: application/json (see ).

    Applicable endpoints: JmWorkOrder, MoMediaOrder, XmTransmissionOrder

    Loads a Work Order Template to an order. This is the improved method (from release 26.1) that can append or replace existing transactions.

    Options: 1 = append template transactions to existing order transactions; 0 = replace existing order transactions with the template's. Applies to POST, PATCH, and PUT, and requires wo_template_no in the payload.

    The prior method is still supported: saveArgument: {"LoadTemplate":"15"} where the number is the template to load; add "LoadChildTemplates":"Y" (from release 11.3) to load child templates. The template number must be in both the header and the payload.

    For transmission orders, load a Service Template and populate service_template_no:

    Load multiple by comma-separating the values; in that case you may leave service_template_no null in the body:

    Applicable endpoints: JmWorkOrder, MoMediaOrder, XmTransmissionOrder

    When a call changes an order's wo_begin_dt and/or wo_end_dt, the app normally asks the user how existing order transactions should react. There's no one to ask over the API, so by default the transactions are left unchanged. Use one of these save arguments to choose a behavior instead:

    Only the key's presence is checked, so any value works. TrxTimeOptionMove only applies when wo_begin_dt changes without a wo_end_dt change in the same call; use TrxTimeOptionAdjust otherwise.

    Applicable endpoints: JmWorkOrder, MoMediaOrder, XmTransmissionOrder

    The value is the Work Order number-sequence. Example with PATCH:

    Applicable endpoints: JmWorkOrder, MoMediaOrder, XmTransmissionOrder

    Applicable endpoint: JmActual (v11.1)

    Actualises selected or all transactions, updating the transaction and order phase (effectively posting the actuals).

    Intended to be used with a POST of actual actions; the save argument then performs the actualisation.

    Applicable endpoint: TcBatch (v11.1)

    Replicates the UI action to 'Post' the batch.

    Applicable endpoint: SchResource

    Sets the default Group of a resource. The Group must already be assigned to the resource.

    Applicable endpoint: BidVersion

    Changes the approval state of a Bid, using a number for the approval type.

    Value
    Approval type

    Where applicable, combine multiple save arguments in one object:

    Performance recommendations

    To avoid impacting the user experience or other integrations, use the API efficiently:

    1. Keep your field projection tight - fields (GET) is required anyway, so list only the columns you actually use; on POST /query use resultColumns. See .

    Suppress null fields - use the nullvaluehandling=ignore parameter to drop null values from responses.
  • Use Webhooks, not polling - subscribe to changes instead of repeatedly querying. See the Webhooks guide.

  • Enable compression - send Accept-Encoding: gzip (or deflate).

  • Always page - v3 applies a default page size automatically; work with it and page through large result sets rather than trying to pull everything at once. See Paging and sorting.

  • Filtering server-side (with query) and projecting only the columns you need are the two highest-impact habits - they cut both database work and payload size.

    Querying list documents (GET)

    3

    ApproveAndUnApproveOriginal

    4

    Abort

    0

    Approval

    1

    Unapproval

    2

    ApproveAsChangeMemo

    Load a template to an order

    Load a service template

    Adjust or move transaction times when order times change

    Void a work order

    Un-void a work order

    Actualise work order actuals

    Post a time card batch

    Set the default group of a scheduling resource

    Approve a bid

    Multiple save arguments

    Creating and updating documents
    saveArgument: {"LoadTemplateOption":1}
    
    // also load child templates
    saveArgument: {"LoadTemplateOption":1,"LoadChildTemplates":"Y"}
    curl --location --globoff 'https://<your-server>/api/v3/JmWorkOrder' \
      --header 'Content-Type: application/json' \
      --header 'saveArgument: {"LoadTemplateOption":1,"LoadChildTemplates":"Y"}' \
      --header 'Authorization: Bearer <access_token>' \
      --data '{
        "jm_work_order": [
          {
            "wo_no_seq": { "wo_no_seq": "+1" },
            "wo_desc": "Match2006",
            "wo_begin_dt": "2026-08-22T10:00:00.000Z",
            "wo_end_dt": "2026-08-22T16:00:00.000Z",
            "wo_template_no": { "wo_template_no": 1050 },
            "wo_type_no": { "wo_type_no": 3275 },
            "phase_code": { "phase_code": "Hold" },
            "rate_card_no": { "rate_card_no": 1 },
            "cust_id": { "cust_id": "1157" }
          }
        ]
      }'
    saveArgument: {"LoadServiceTemplate":"10"}
    saveArgument: {"LoadServiceTemplate":"10","11"}
    // shift transaction times by the same amount as the order time change
    saveArgument: {"TrxTimeOptionAdjust":""}
    
    // shift transaction times to follow the order's new begin time exactly
    saveArgument: {"TrxTimeOptionMove":""}
    curl --location --request PATCH 'https://<your-server>/api/v3/JmWorkOrder/wo_no_seq/2626-1' \
      --header 'Content-Type: application/json-patch+json' \
      --header 'saveArgument: {"TrxTimeOptionAdjust":""}' \
      --header 'Authorization: Bearer <access_token>' \
      --data '[
        { "op": "replace", "path": "wo_begin_dt", "value": "2026-08-22T14:00:00.000Z" },
        { "op": "replace", "path": "wo_end_dt", "value": "2026-08-22T20:00:00.000Z" }
      ]'
    saveArgument: {"VoidWorkOrder":"36914-1"}
    curl --location --request PATCH 'https://<your-server>/api/v3/JmWorkOrder/wo_no_seq/2626-1' \
      --header 'Content-Type: application/json-patch+json' \
      --header 'saveArgument: {"VoidWorkOrder":"2626-1"}' \
      --header 'Authorization: Bearer <access_token>' \
      --data '[
        { "op": "replace", "path": "cancel_no", "value": 1 }
      ]'
    saveArgument: {"UnVoidWorkOrder":"1067982-1"}
    curl --location --request PATCH 'https://<your-server>/api/v3/JmWorkOrder/wo_no_seq/1067982-1' \
      --header 'Content-Type: application/json-patch+json' \
      --header 'saveArgument: {"UnVoidWorkOrder":"1067982-1"}' \
      --header 'Authorization: Bearer <access_token>' \
      --data '[]'
    saveArgument: { "ActualizeSelected":"38521,38522", "ActualizeUpdatePhase":"Y" }
    saveArgument: { "ActualizeAll":"-2", "ActualizeUpdatePhase":"Y" }
    saveArgument: {"Post":""}
    curl --location --request PATCH 'https://<your-server>/api/v3/TcBatch/batch_no/7572' \
      --header 'Content-Type: application/json-patch+json' \
      --header 'saveArgument: {"Post":""}' \
      --header 'Authorization: Bearer <access_token>' \
      --data '[]'
    saveArgument: {"GroupCode":"UKPS"}
    curl --location --request PATCH 'https://<your-server>/api/v3/SchResource/resource_code/3' \
      --header 'Content-Type: application/json-patch+json' \
      --header 'saveArgument: {"GroupCode":"TRX"}' \
      --header 'Authorization: Bearer <access_token>' \
      --data '[]'
    saveArgument: {"BidApproval":"0"}
    curl --location --request PATCH 'https://<your-server>/api/v3/BidVersion/version_no/211' \
      --header 'Content-Type: application/json-patch+json' \
      --header 'saveArgument: {"BidApproval":"0"}' \
      --header 'Authorization: Bearer <access_token>' \
      --data '[
        { "op": "replace", "path": "approved", "value": "Y" }
      ]'
    saveArgument: {"LoadServiceTemplate":"10","LoadTemplate":"2"}

    Methods and response codes

    The REST API uses standard HTTP methods to distinguish the type of operation.

    Verb
    Used for

    PATCH

    Update an existing record. Two body formats: application/json-patch+json takes an array of {op, path, value} field operations; application/json takes a document-shaped payload. See .

    PUT

    Upsert - create the record, or update it if it already exists (matched on external_key). application/json + document payload, keyed URL.

    DELETE

    Permanently remove a record. Use sparingly - to preserve data integrity and history it is usually better to change a record's status than to delete it.

    v3 change: record keys are addressed as path segments (/JmJob/job_no/123), and the PATCH content-type mapping is reversed from v2 - see Migrating from API v2 to API v3.

    The API returns standard HTTP status codes. In v3, client errors are mapped to the correct status (not a blanket 400) and the response body carries a machine-readable code.

    Range
    Meaning

    2xx

    Success - 200 OK, 201 Created, 204 No Content

    4xx

    Client error - 400, 401, 403, 404 (each with an XY_* code)

    5xx

    Server error - 500

    For the full v3 status-code and code catalogue, and recommended handling, see Error responses.

    GET

    Retrieve a specific record (e.g. a Job by its ID), or a list of records that match a query (e.g. all Bids starting this year). See Addressing a document and Querying list documents (GET).

    POST

    Create a new record. Also used to run a complex query against a list/report endpoint via the /query route - see Querying with POST.

    Methods

    Response codes

    Creating and updating documents

    Key fields and time formats

    Key field sub-sections

    Each payload contains a nested sub-section of key fields, used to look records up by their key. This sub-section is identified by the primary key field name, and always contains the primary key field plus the external_key field.

    The key sub-section is not used for writing values to the database - values you want to store must live in the root of the payload. As a general rule, if you are creating or updating field values, put those fields in the root of the payload.

    There are also sub-sections for related tables with their key fields. Look-ups (via GET query parameters) can only filter sub-tables using the sub-table primary key, not their alternate keys, unless the sub-table field also exists in the root of the payload.

    Creating records with key fields

    Using POST you have two ways to set the key values of sub-tables. This example creates a JmJob, generating the primary key (with -1) and assigning the Division, Customer, and Job Type sub-table IDs - mirroring the structure a GET returns:

    Alternatively, flatten the structure so the field name matches the sub-section name:

    When creating records with POST, values for alternate key fields such as external_key must be in the root of the payload, not in the key sub-section (where they are ignored). The same applies to a document-payload PATCH (Content-Type: application/json).

    The external_key field exists on all documents and is designed to hold the external system's unique identifier for a record. You can then use it in later look-ups instead of having to know the Xytech primary key value.

    Some documents have additional key fields, all held in the same key sub-section - for example Media Assets (lib_master) expose master_no, barcode, external_key, and umid. Any of these can retrieve the record with a GET. Using the barcode alternate key:

    Ensure alternate-key uniqueness if you want to retrieve individual records this way. If a key value can contain a / or other special character, use the /where/ route - see .

    When creating records with POST, set the primary key value to -1 to generate it. When creating multiple sub-table records in one call that each need a generated value, decrement for each: -1, -2, -3, . . Reusing -1 refers to the same generated value again, which is useful when the same new ID must be populated elsewhere in the payload.

    Prior to v11.3 there is one exception: JmWorkOrder, whose primary key is wo_no + wo_seq; to generate wo_no_seq use a "+1" value:

    Some fields are mandatory (defined in the OpenAPI specification). The ID of the created record is returned in the response payload.

    Date-time values follow ISO-8601: YYYY-MM-DDThh:mm:ss.sTZD.

    Times are stored in the Xytech database as UTC (the exception is the legacy option where the Master Time Zone is not UTC). The recommendation is to work in UTC, or in local times using the time-zone header.

    POST / PUT / PATCH - supply times as UTC:

    or with a time-zone offset:

    or supply local times and name the zone with a header - any DST offset is calculated automatically when the UTC value is stored (this overrides any offset in the value):

    GET / responses - times are returned with an offset attribute, e.g. 2025-02-20T10:00:00+00:00 (the offset is determined by the app-server time zone, normally UTC; time plus offset equals the UTC time).

    Alternate keys

    External key

    Generating the primary key

    Time formats

    Addressing a document
    {
        "jm_job": [
            {
                "job_no": { "job_no": -1 },
                "division_no": { "division_no": 2 },
                "cust_id": { "cust_id": 409 },
                "job_desc": "Passing Fancy",
                "external_key": "1234",
                "po": "PO002",
                "job_type_no": { "job_type_no": 1113 }
            }
        ]
    }
    {
        "jm_job": [
            {
                "job_no": -1,
                "division_no": 2,
                "cust_id": "409",
                "job_desc": "Passing Fancy",
                "external_key": "1234",
                "po": "PO002",
                "job_type_no": 1113
            }
        ]
    }
    {
        "sch_resource": [
            {
                "resource_code": { "resource_code": "FRED01" },
                "resource_desc": "Fred",
                "external_key": "Ext001"
            }
        ]
    }
    GET https://<your-server>/api/v3/LibMaster/barcode/2000XYT
    {
        "jm_work_order": [
            {
                "wo_no_seq": "+1",
                "wo_desc": "Match20",
                "wo_begin_dt": "2023-08-22T10:00:00.000Z",
                "wo_end_dt": "2023-08-22T16:00:00.000Z",
                "wo_type_no": 17,
                "phase_code": "Bid",
                "rate_card_no": 1,
                "cust_id": 19
            }
        ]
    }
    2025-02-20T10:00:00.000Z
    2025-02-20T10:00:00Z
    2025-02-16T04:00:00-05:00
    Source-Time-Zone-Name: Pacific Standard Time

    Filtering reference

    Operator names are spelled differently in the two verbs, and the spellings are not interchangeable. In a POST /query body, a condition's op takes an unprefixed name - gte, in, contains. The GET query parameter requires the $-prefixed form - $gte, $in. Sending an unprefixed operator on GET, or an unrecognised operator on either verb, is rejected with a 400 listing the valid names.

    The same condition, in each verb:

    contains has no GET operator of its own - put the wildcards in the value instead. { "field": "contact", "op": "contains", "value": "Lee" } on POST is equivalent to { "contact": "%Lee%" } on GET.

    $likeAnd and $or are GET-only; they have no POST equivalent. In a POST body, OR logic is expressed with combiningOperator on a group - see .

    Every column type can be filtered, including decimal and currency columns. The restriction is on how the value is written in JSON, not on what the column holds.

    A quoted decimal is still compared numerically, not as text.

    Two consequences worth knowing:

    • The type check is not applied uniformly. in, notin and range validate only that a value is present, not its type. An unquoted 10.5 is refused by eq but passed through by in, and what happens next depends on the column - fine on a decimal column, a database conversion error on an integer one. Quoting sidesteps the difference.

    Comparison operators work on text columns too, comparing alphabetically - contact gte "M" is valid.

    • Multiple keys in one object are combined with AND:

    • Use $or for alternatives:

    • In a POST body, use nested filter groups with combiningOperator (and

    Conditions naming the same field are combined. Combinations that cannot be represented are rejected rather than approximated:

    Combination
    Result

    The same field may appear in different OR branches without restriction. These rules apply within a single AND scope.

    Two rules that regularly surprise callers:

    1. Only gte, lte, range and isnull work on a date column. eq, in and contains are rejected with "Date parameters must use range syntax".

    A document defines two column sets: the columns it can return and the columns it can be filtered by. They overlap heavily but are not the same, and the filterable set is the smaller one.

    A column that exists in the table - and even appears in resultColumns - may still be refused as a filter. unit_increment on the Billing Code list can be returned but not filtered; the amount columns on jm_job are not filterable from the job list.

    So when a filter is rejected as an undefined parameter, the question is not whether the column exists, but whether that document exposes it as a filter column. There is no API call that lists the filterable set - it comes from the document definition.

    A filter is either applied exactly as written, or the request is rejected with a message naming the field. These all return 400:

    • an unknown or misspelled operator, including an unprefixed operator on GET

    • a condition with no field

    • contains given a list instead of a single value

    Field selection is required on every v3 GET (see ):

    • GET: the fields parameter as a JSON object - fields={"jm_job":["job_no","customer_name"]}.

    • POST: the fields array, e.g. [{ "table": "jm_job", "columns": ["job_no", "customer_name"] }]. resultColumns is still accepted as a deprecated alias.

    Projecting only the fields you need makes responses smaller and faster.

    $ne

    Not equal

    { "status": { "$ne": 1 } }

    gt

    $gt

    Greater than

    { "quantity": { "$gt": 100 } }

    gte

    $gte

    Greater than or equal

    { "quantity": { "$gte": 100 } }

    lt

    $lt

    Less than

    { "quantity": { "$lt": 100 } }

    lte

    $lte

    Less than or equal

    { "quantity": { "$lte": 100 } }

    in

    $in

    In a set

    { "status": { "$in": [1, 2, 3] } }

    notin

    $notin

    Not in a set

    { "status": { "$notin": [4, 5] } }

    range

    $range

    Between two values (inclusive)

    { "startDate": { "$range": ["2026-01-01", "2026-01-31"] } }

    contains

    (wildcards)

    Text contains

    { "contact": "%Lee%" }

    isnull

    $isnull

    Field is null

    { "closedDate": { "$isnull": true } }

    not available

    $likeAnd

    Text contains all the given words

    { "description": { "$likeAnd": "urgent project" } }

    use groups

    $or

    Combine sub-conditions with OR

    { "$or": [ { "status": 1 }, { "priority": { "$gte": 8 } } ] }

    "10.5" - quoted decimal

    Accepted

    Accepted

    10.5 - unquoted decimal

    Rejected

    Not type-checked

    "2026-06-01" - date as text

    Accepted

    Accepted

    true / false

    Rejected - except isnull, which requires it

    Not type-checked

    true inside an in list is not rejected and will not match. It is passed through as the text "True", while checkbox columns hold "Y" / "N", so the condition matches nothing and returns an empty result with no error. Filter checkbox columns with "Y" or "N".
    /
    or
    ) for full control - see
    .

    range + any bound

    Rejected - the bound is already set

    ne + ne, notin, isnull false

    Combined with AND - every exclusion applies

    contains + contains

    Combined with AND - both patterns must match

    eq + eq

    Rejected - two equalities read as "either", not "both". Use in for "either of"

    eq + in, eq + isnull, in + contains

    Rejected for the same reason

    Any range bound + any equality

    Rejected - a field holds one or the other

    range and a pair of bounds are not equivalent on dates. range treats a midnight upper bound as exclusive; lte includes the whole final day.

    range ["2026-06-01","2026-06-30"] stops at 30 June 00:00, so 30 June is excluded. gte "2026-06-01" plus lte "2026-06-30" includes all of 30 June.

    Neither is wrong - they ask different questions. Pick the one matching your intent.

    in, notin or range with no values

  • any of the un-representable same-field combinations above

  • an or group nested inside another OR branch

  • POST op

    GET operator

    Meaning

    Example (GET query)

    eq (default)

    (none)

    Exact match

    { "status": 1 }

    Written as

    eq ne gt gte lt lte contains

    in notin range

    10035 - whole number

    Accepted

    Accepted

    "10035" - quoted

    Accepted

    gte + lte (both inclusive)

    Combined into a range

    gt + lt (both exclusive)

    Combined into a range

    gt + lte - one exclusive, one inclusive

    Rejected - a range carries one inclusive/exclusive setting for both ends. Use range instead

    Operators

    Writing filter values

    Rule of thumb: put filter values in quotes. Quoted values are accepted by every operator for every column type. The one exception is isnull, which requires a real true / false.

    Combining conditions

    Two conditions on the same field

    POST /query only. A GET query object holds one operator per field, so the situation cannot arise.

    Date and date-time columns

    Which columns can be filtered

    What is rejected

    Selecting which fields come back

    Querying with POST
    Querying list documents

    ne

    Accepted

    Querying with POST
    { "quantity": { "$gte": 100 } }                          // GET  ?query=
    { "field": "quantity", "op": "gte", "value": 100 }       // POST /query
    { "status": 1, "priority": { "$gte": 5 } }   // status = 1 AND priority >= 5
    { "$or": [ { "status": 1 }, { "priority": { "$gte": 8 } } ] }

    Authentication

    Every v3 request must carry an Authorization header with a bearer token. There are two ways to obtain that token - the same two methods used across the Xytech REST API:

    • Auth-provider OAuth (recommended) - obtain a bearer token from your configured identity provider (Azure AD, Okta, or Auth0) and use it to call the API. This is the standard method for both system-to-system and interactive integrations.

    • Xytech token endpoint - exchange a Xytech username and password for a Xytech-issued token via the API's own auth endpoints.

    The methods your server accepts are controlled by its REST_AUTH_METHODS configuration.

    Your auth provider and the Xytech app server must already be configured (Azure and Okta/Auth0). Two provider flows are supported:

    • Client Credentials (system-to-system): you need the Access token URL, Client ID, Client Secret, and Scope.

    • SPA / browser sign-in (interactive user sign-in): you need the Authorisation URL, Access token URL, Client ID, Client Secret, and Scope.

    The flow is the same in both cases:

    1. Obtain a token from your auth provider.

    2. Send it as a bearer token on every REST API call until it expires, then obtain a new one.

    See the existing Connecting and Authenticating page for provider-specific setup - the v3 API accepts these tokens unchanged.

    The API can also issue its own tokens. Exchange a username and password for an access token, a refresh token, and an ID token:

    Interim note. The orchestration/auth endpoints have not yet been migrated to v3, so they are served by API v2 and still require the /database/{name}/ segment - unlike v3 data calls, which take no database. This is a short-term arrangement; a future release will bring these endpoints in line with the v3 standard. The token they return works against v3 data endpoints. (If you authenticate with an auth-provider bearer token or Basic auth - Method 1 - you don't need this endpoint at all.)

    Response

    Send the accessToken as a bearer token on subsequent requests:

    When the access token expires, exchange it for a new one. The refresh request requires both the current accessToken and the refreshToken:

    The response is the same shape as login (accessToken, refreshToken, idToken).

    All are under ./api/v2/database/<your-database>/orchestration/auth:

    Method & path
    Purpose
    Returns

    See for the full catalogue.

    GET .../auth/validate

    Check the current token is valid

    The authenticated user's name

    GET .../auth/me

    Return the authenticated user's profile

    User info

    POST .../auth/login

    Exchange username/password for tokens

    { accessToken, refreshToken, idToken }

    POST .../auth/refresh

    Exchange accessToken + refreshToken for new tokens

    { accessToken, refreshToken, idToken }

    Status

    code

    Meaning

    401 Unauthorized

    XY_UNAUTHORIZED

    No credentials, malformed header, or an expired/invalid token

    403 Forbidden

    XY_FORBIDDEN

    Method 1 - bearer token from your auth provider (recommended)

    Method 2 - Xytech token endpoints

    Refreshing a Xytech token

    Other auth endpoints

    Common auth failures

    Error responses

    Authenticated, but the user isn't permitted to perform the action

    GET https://<your-server>/api/v3/JmJob/job_no/123
    Authorization: Bearer <provider_access_token>
    POST https://<your-server>/api/v2/database/<your-database>/orchestration/auth/login
    Content-Type: application/json
    
    {
      "username": "myuser",
      "password": "mypassword"
    }
    {
      "accessToken": "<jwt>",
      "refreshToken": "<guid>",
      "idToken": "<jwt>"
    }
    Authorization: Bearer <accessToken>
    POST https://<your-server>/api/v2/database/<your-database>/orchestration/auth/refresh
    Content-Type: application/json
    
    {
      "accessToken": "<current_accessToken>",
      "refreshToken": "<refreshToken>"
    }