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 addInitialize 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_urltrims a trailing slash; defaults tohttps://gateway.odxproxy.io.default_timeout_secsis sent asx-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.
| Action | Function | params shape | result |
|---|---|---|---|
search_count | search_count | [[domain]] | number |
search | search | [[domain]] | number[] (IDs) |
read | read | [[ids], [fields]] | records |
fields_get | fields_get | (model, keyword) | field metadata |
search_read | search_read | [[domain]] | records |
create | create | [{ fields }] | number (new ID) |
write | write (alias update) | [[ids], { fields }] | boolean |
unlink | remove | [[ids]] | boolean |
call_method | call_method | method args array | method 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.
| Class | Proxy code | Meaning |
|---|---|---|
AuthError | -32000 | Missing or wrong x-api-key. |
InvalidActionError | -32001 | Action not in the allowlist. |
MissingFnNameError | -32002 | call_method without fn_name. |
OdooTimeoutError | -32003 | Upstream Odoo timed out (also on client abort ceiling). |
OdooConnectError | -32004 | Network failure reaching Odoo. |
InternalProxyError | -32005 | Internal proxy error. |
LicenseError | 0 (HTTP 403) | Proxy integrity/license check failed. |
OdooLogicError | Odoo'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.