npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vvhybe/odoo-client

v0.1.0

Published

Type-safe JSON-RPC and XML-RPC client for Odoo — works with Node.js, Next.js, and React

Readme

Type-safe JSON-RPC and XML-RPC client for Odoo — works with Node.js, Next.js, React and any modern JS runtime.

  • Dual protocol — JSON-RPC (/web/dataset/call_kw) and XML-RPC (/xmlrpc/2/object) in one package
  • Full TypeScript — typed domains, field values, ORM options, and error classes
  • Auth strategies — session/password auth or API key (Odoo 14+)
  • Retry & timeout — exponential backoff on network failures; auth errors are never retried
  • Async paginationorm.paginate() async generator for bulk record processing
  • Zero dependencies — uses native fetch (Node 18+, all browsers)
  • Typed errorsOdooAuthenticationError, OdooAccessError, OdooValidationError, etc.

OdooConnect is the single entry point. It wires together three layers:

Services sit in the middle and own all business logic — AuthService handles the three auth paths (session, XML-RPC password, API key), OrmService translates your method calls into the correct RPC payloads and handles version-specific API differences between Odoo 14 and 19.

Clients at the bottom do exactly one thing: fire an HTTP request and deserialise the response. JsonRpcClient owns the session cookie across calls in Node.js environments. XmlRpcClient includes a zero-dependency XML serialiser/deserialiser.

Shared infra (retry, typed errors) is used by both clients. Auth errors are never retried — they surface immediately. Network errors back off exponentially.

The protocol is hot-swappable — set protocol: 'xmlrpc' and the same ORM API routes through XML-RPC. The service layer never knows the difference.

  • Node.js ≥ 18 (for native fetch and DOMParser)
  • Odoo 14, 15, 16, 17, 18, 19
npm install @vvhybe/odoo-client
# or
pnpm add @vvhybe/odoo-client
# or
yarn add @vvhybe/odoo-client
import { OdooConnect } from '@vvhybe/odoo-client';

// Option A: factory method (authenticates immediately)
const odoo = await OdooConnect.connect({
  url: 'https://mycompany.odoo.com',
  db: 'mydb',
  username: '[email protected]',
  password: 'secret',
});

// Option B: manual
const odoo = new OdooConnect({ url, db, username, password });
await odoo.authenticate();

// CRUD
const partners = await odoo.orm.searchRead('res.partner', [['is_company', '=', true]], {
  fields: ['name', 'email', 'phone'],
  limit: 50,
  order: 'name asc',
});

const id = await odoo.orm.create('res.partner', { name: 'Acme Corp', is_company: true });
await odoo.orm.write('res.partner', [id], { phone: '+1 555 0100' });
await odoo.orm.unlink('res.partner', [id]);
interface OdooConfig {
  url: string;           // Base URL — e.g. https://mycompany.odoo.com
  db: string;            // Database name
  username?: string;     // Required for password/apiKey auth
  password?: string;     // Required for password auth
  apiKey?: string;       // Alternative to password (Odoo 14+)
  protocol?: 'jsonrpc' | 'xmlrpc';  // Default: 'jsonrpc'
  timeout?: number;      // ms. Default: 30000
  retries?: number;      // Default: 3
  retryDelay?: number;   // Base backoff ms. Default: 500
  context?: OdooContext; // Merged into every RPC call
}

| Method | Description | | --- | --- | | new OdooConnect(config) | Create instance (does not authenticate) | | OdooConnect.connect(config) | Create + authenticate in one step | | odoo.authenticate() | Authenticate and return the session | | odoo.getSession() | Returns current OdooSession or null | | odoo.disconnect() | Clears the session | | odoo.orm | OrmService — see below | | odoo.auth | AuthService — low-level auth access | | odoo.jsonRpc | JsonRpcClient — direct RPC access | | odoo.xmlRpc | XmlRpcClient — direct RPC access |

// Search + read in one call
orm.searchRead<T>(model, domain?, options?): Promise<T[]>

// Read by ids
orm.read<T>(model, ids, options?): Promise<T[]>

// Read one record — throws OdooNotFoundError if missing
orm.readOne<T>(model, id, options?): Promise<T>

// Search and return ids
orm.search(model, domain?, options?): Promise<number[]>

// Count matching records
orm.searchCount(model, domain?, context?): Promise<number>

// Autocomplete helper: returns [(id, display_name), ...]
orm.nameSearch(model, name, domain?, limit?, context?): Promise<[number, string][]>

// Async generator — yields pages of records
orm.paginate<T>(model, domain?, options?): AsyncGenerator<T[]>
// Create one record — returns new id
orm.create(model, values, context?): Promise<number>

// Create multiple records — returns list of ids
orm.createMany(model, valuesList, context?): Promise<number[]>

// Update records
orm.write(model, ids, values, context?): Promise<boolean>

// Delete records
orm.unlink(model, ids, context?): Promise<boolean>
// Get field definitions
orm.fieldsGet(model, attributes?, context?): Promise<OdooFieldsGet>

// Call any model method
orm.callMethod<T>(model, method, args?, kwargs?, context?): Promise<T>

Domains follow Odoo's standard domain format:

import type { OdooDomain } from '@vvhybe/odoo-client';

const domain: OdooDomain = [
  ['is_company', '=', true],
  ['country_id.code', '=', 'MA'],
];

// Logical operators
const complex: OdooDomain = [
  '|',
  ['email', 'ilike', '@gmail.com'],
  ['email', 'ilike', '@yahoo.com'],
];

Supported leaf operators: =, !=, >, >=, <, <=, like, ilike, not like, not ilike, in, not in, child_of, parent_of, =like, =ilike.

import type { TypedOdooRecord } from '@vvhybe/odoo-client';

interface ResPartner {
  name: string;
  email: string;
  phone: string | false;
  is_company: boolean;
  country_id: [number, string] | false;
}

const partners = await odoo.orm.searchRead<TypedOdooRecord<ResPartner>>(
  'res.partner',
  [['is_company', '=', true]],
  { fields: ['name', 'email', 'phone', 'country_id'] },
);

// partners[0].name is string ✓
// partners[0].id is number ✓
// Process all active products in pages of 200
for await (const page of odoo.orm.paginate('product.product', [['active', '=', true]], {
  fields: ['name', 'default_code', 'list_price'],
  pageSize: 200,
})) {
  await syncToDatabase(page);
  console.log(`Synced ${page.length} products`);
}
const odoo = await OdooConnect.connect({
  url: 'https://mycompany.odoo.com',
  db: 'mydb',
  username: '[email protected]',
  apiKey: process.env.ODOO_API_KEY,
});

Generate API keys in Odoo under Settings → Users → your user → API Keys.

const odoo = await OdooConnect.connect({
  url: 'https://mycompany.odoo.com',
  db: 'mydb',
  username: '[email protected]',
  password: 'secret',
  protocol: 'xmlrpc',           // ← switch here; the rest of the API is identical
});

const ids = await odoo.orm.search('sale.order', [['state', '=', 'sale']]);
import {
  OdooAuthenticationError,
  OdooAccessError,
  OdooValidationError,
  OdooNotFoundError,
  OdooNetworkError,
  OdooTimeoutError,
  OdooRpcError,
} from '@vvhybe/odoo-client';

try {
  await odoo.orm.create('res.partner', { name: '' });
} catch (error) {
  if (error instanceof OdooValidationError) {
    console.error('Validation failed:', error.message);
  } else if (error instanceof OdooAccessError) {
    console.error('Permission denied:', error.message);
  } else if (error instanceof OdooNetworkError) {
    console.error('Network issue:', error.message, error.cause);
  } else if (error instanceof OdooTimeoutError) {
    console.error('Timed out after', error.timeoutMs, 'ms');
  }
}
// lib/odoo.ts (server-only)
import { OdooConnect } from '@vvhybe/odoo-client';

let _client: OdooConnect | null = null;

export async function getOdoo() {
  if (_client?.getSession()) return _client;
  _client = await OdooConnect.connect({
    url: process.env.ODOO_URL!,
    db: process.env.ODOO_DB!,
    username: process.env.ODOO_USERNAME!,
    password: process.env.ODOO_PASSWORD!,
  });
  return _client;
}
// app/api/partners/route.ts
import { NextResponse } from 'next/server';
import { getOdoo } from '@/lib/odoo';

export async function GET() {
  const odoo = await getOdoo();
  const partners = await odoo.orm.searchRead('res.partner', [], {
    fields: ['name', 'email'],
    limit: 100,
  });
  return NextResponse.json(partners);
}
// JSON-RPC — call a custom Odoo controller
const result = await odoo.jsonRpc.callPath('/my_module/custom_endpoint', {
  model: 'my.model',
  record_id: 42,
});

// XML-RPC — call a non-standard method directly
const result = await odoo.xmlRpc.executeKw(
  'mydb', uid, 'password',
  'my.model', 'my_custom_method',
  [[42]], { option: true },
);
import type { OdooCommand } from '@vvhybe/odoo-client';

await odoo.orm.write('sale.order', [orderId], {
  order_line: [
    [0, 0, { product_id: 7, product_uom_qty: 3, price_unit: 99.99 }], // CREATE
    [1, 55, { product_uom_qty: 5 }],                                   // UPDATE
    [2, 60, 0],                                                        // DELETE
  ] satisfies OdooCommand[],
});
ODOO_URL=https://mycompany.odoo.com
ODOO_DB=mydb
[email protected]
ODOO_PASSWORD=secret
# or
ODOO_API_KEY=your-api-key

Please see CONTRIBUTING.md for details on how to get started.

Please follow the Conventional Commits format.

To ensure a healthy and welcoming community, we adhere to the following standards:

  • [ ] ReportService — render and download PDF/XLSX reports
  • [ ] WebsocketService — Odoo bus real-time subscriptions
  • [ ] Batch request support (single HTTP round-trip for multiple calls)
  • [ ] React hooks package (@vvhybe/odoo-client-react)
  • [ ] Model type generator from fields_get output

MIT © whybe