ODXProxy
SDKs

JavaScript SDK

A zero-dependency, typed JavaScript/TypeScript client for ODXProxy — init once, then call typed helpers.

@terrakernel/odxproxy-client-js is the official JavaScript/TypeScript client for ODXProxy. It's a thin, zero-runtime-dependency wrapper over the platform fetch: initialize a singleton once, then call typed helper functions (search_read, create, …). Every failure is thrown as a typed error.

Source: terrakernel/odxproxy-client-js · npm @terrakernel/odxproxy-client-js · Node 18+ or any modern browser · ships ESM + CJS + types.

Install

npm install @terrakernel/odxproxy-client-js
# or: yarn add / pnpm add

Initialize once

Call init a single time at startup, then import helpers anywhere. Calling init twice throws ("OdxProxyClient has already been initialized."); calling a helper before init throws ("OdxProxyClient has not been initialized.").

import { init, search_read } from "@terrakernel/odxproxy-client-js";

init({
  instance: {
    url: process.env.ODOO_URL!,      // your Odoo base URL
    db: process.env.ODOO_DB!,        // Odoo database name
    user_id: 2,                      // Odoo user id (integer)
    api_key: process.env.ODOO_API_KEY!,  // the Odoo USER's api key
  },
  odx_api_key: process.env.ODX_API_KEY!, // the PROXY's x-api-key
  gateway_url: "https://your-proxy.example.com", // optional; default https://gateway.odxproxy.io
  default_timeout_secs: 15,               // optional; sent as x-request-timeout
});

Two different secrets

odx_api_key is the proxy's x-api-key; instance.api_key is the Odoo user's API key. They are never the same value — don't conflate them. Note the field names are snake_case (odx_api_key, gateway_url, user_id, api_key), matching the wire protocol.

Keep the keys server-side

Both secrets travel with every request (the proxy key as a header, the Odoo key inside the body). Initializing the client in untrusted browser code exposes both to end users. Prefer running it in a Node backend / trusted context; treat browser use as internal/trusted apps only.

  • gateway_url trims a trailing slash; defaults to https://gateway.odxproxy.io.
  • default_timeout_secs is sent as x-request-timeout; the client also aborts ~5s past that ceiling (45s when unset).

Fetch records

Helpers resolve to the JSON-RPC envelope { jsonrpc, id, result? } on success and throw on any failure. search_read is the common one — filter and read in one call. Pass a context (with at least tz) inside keyword:

type Partner = { id: number; name: string; email?: string };

const res = await search_read<Partner>(
  "res.partner",
  [[["is_company", "=", true]]],           // domain (params)
  { fields: ["id", "name", "email"], limit: 50, context: { tz: "UTC" } }
);

const partners = res.result ?? [];

The generic is the record type; search_read<Partner> resolves to result?: Partner[]. Every helper also takes an optional trailing opts for per-call control:

const ac = new AbortController();
await search_read("res.partner", [[]], { context: { tz: "UTC" } }, undefined, {
  timeoutSecs: 60,   // overrides default_timeout_secs for this call
  signal: ac.signal, // cancel via ac.abort()
});

For every action except search_read, the client strips fields / order / limit / offset from keyword before sending (they only apply to a combined search+read). context is always sent. The sort key is order (e.g. "name asc").

The 9 actions, mapped

Each allowed action is an exported function. Signature: fn(model, params, keyword, id?, opts?) — except call_method, which takes function_name after keyword.

ActionFunctionparams shaperesult
search_countsearch_count[[domain]]number
searchsearch[[domain]]number[] (IDs)
readread[[ids], [fields]]records
fields_getfields_get(model, keyword)field metadata
search_readsearch_read[[domain]]records
createcreate[{ fields }]number (new ID)
writewrite (alias update)[[ids], { fields }]boolean
unlinkremove[[ids]]boolean
call_methodcall_methodmethod args arraymethod return
import { create, write, remove, call_method } from "@terrakernel/odxproxy-client-js";

const kw = { context: { tz: "UTC" } };

const created  = await create("res.partner", [{ name: "Acme Inc", is_company: true }], kw);
const updated  = await write("res.partner",  [[42], { name: "Acme LLC" }], kw);
const deleted  = await remove("res.partner", [[42]], kw);

// call_method: function_name is the 4th positional arg (after keyword)
const confirmed = await call_method<boolean>("sale.order", [[42]], kw, "action_confirm");

call_method rejects with MissingFnNameError (proxy code -32002) if function_name is empty — no wasted round-trip. Anything outside the 9 actions must go through call_method. Mind the argument order: function_name comes after keyword, not before.

read takes its field list positionally ([[1, 2, 3], ["name", "email"]]) — fields in keyword is ignored for read.

Auxiliary endpoints

import { version, about, license, metrics } from "@terrakernel/odxproxy-client-js";

const v   = await version("https://erp.example.com"); // envelope; url is required
const a   = await about();      // envelope: { build, version }
const lic = await license();    // OdxLicenseInfo — NOT an envelope
const txt = await metrics();    // Prometheus text (string)

license() resolves to a flat OdxLicenseInfo ({ licensee, valid_until, is_valid }), not a JSON-RPC envelope — the proxy's /_/license endpoint emits a flat object. about, license, and metrics need no API key.

Typed errors

Every failure — proxy-level (non-2xx) and Odoo logic errors (a 200 carrying an error body) — is thrown as an OdxError subclass. Branch with instanceof. See the full error catalog.

ClassProxy codeMeaning
AuthError-32000Missing or wrong x-api-key.
InvalidActionError-32001Action not in the allowlist.
MissingFnNameError-32002call_method without fn_name.
OdooTimeoutError-32003Upstream Odoo timed out (also on client abort ceiling).
OdooConnectError-32004Network failure reaching Odoo.
InternalProxyError-32005Internal proxy error.
LicenseError0 (HTTP 403)Proxy integrity/license check failed.
OdooLogicErrorOdoo's code (HTTP 200)Odoo business error (validation, access rights).
import { search_read, AuthError, OdooLogicError, OdooTimeoutError, OdxError }
  from "@terrakernel/odxproxy-client-js";

try {
  const res = await search_read("res.partner", [[]], { context: { tz: "UTC" } });
  console.log(res.result);
} catch (err) {
  if (err instanceof AuthError)        { /* bad x-api-key — reauth */ }
  else if (err instanceof OdooLogicError)   { /* Odoo validation / access error */ }
  else if (err instanceof OdooTimeoutError) { /* retry with backoff */ }
  else if (err instanceof OdxError)    { console.error(err.code, err.message, err.httpStatus); }
  else throw err; // not from this SDK (e.g. a caller-initiated AbortError)
}

On every OdxError, code is the JSON-RPC code (the values above) — not the HTTP status, which is on httpStatus. A JSON-RPC error can arrive with HTTP 200 (OdooLogicError); the client inspects the envelope's error field for you, but never infer success from the HTTP status alone if you drop to raw fetch.


Looking for another language? See the SDK overview — every client mirrors this same wire protocol.

On this page