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

@alternative-design-and-media/rentman-api-connector

v2.4.0

Published

Type-safe Rentman REST API client for Node.js and edge runtimes (Cloudflare Workers). Synced to OAS v1.7.0.

Readme

@alternative-design-and-media/rentman-api-connector

v2.3.0

Type-safe Rentman REST API connector for Node.js and edge runtimes (Cloudflare Workers). Synced to OAS v1.7.0 (deployment 2025-11-13).

npm License: MIT


Features

  • ✅ Full TypeScript types for 55+ Rentman resources (RentmanEquipmentItem, RentmanProject, RentmanContact, …)
  • ✅ Typed response wrappers with all pagination metadata (data, itemCount, limit, offset, next_page_url)
  • ✅ Explicit updateHash on every entity for change tracking
  • ✅ Type-safe query builder — fields, sort, relational operators, isnull filters
  • ✅ Cursor-aware auto-pagination helpers for collections larger than 300 items
  • ✅ Preserve-slashes collection helper (listWithPreservedSlashes) for resource-path filters
  • ✅ Token normalization helper (normalizeToken) — accepts bare JWTs and "Bearer ..." tokens
  • ✅ Edge-runtime compatible — uses native fetch only (Node.js 18+, Cloudflare Workers)
  • ✅ Token rotation via callback — no need to recreate the client on token refresh
  • ✅ Kit/set content via rentman.equipment.listSetContents() (+ helper alias listEquipmentSetContents)
  • ✅ Equipment field normalizer (normalizeEquipmentItem) for mixed OAS/legacy payloads
  • ✅ Typed custom fields — narrow custom_<number> keys per entity at compile time
  • Prisma-style typed clientcreateTypedClient integrates account-specific custom fields into the OOP facade
  • ✅ Resource path utilities — parseResourcePath, resourceId, buildResourcePath
  • ✅ Lookup cache helpers — fetchLookupMap, fetchStatusCache, fetchFolderNameCache

Installation

npm install @alternative-design-and-media/rentman-api-connector
# or
pnpm add @alternative-design-and-media/rentman-api-connector

For AI agents / coding assistants


Quick Start

import {
  createRentmanClient,
  projectQuery,
  equipmentQuery,
} from '@alternative-design-and-media/rentman-api-connector';

// Create a client once; reuse everywhere.
const rentman = createRentmanClient({
  token: process.env.RENTMAN_TOKEN!, // bare JWT or "Bearer ..." also accepted
  // or an async function: token: () => getTokenFromVault()
});

// --- OOP facade list with typed query builder ---
const projects = await rentman.projects.listAll(
  projectQuery()
    .startingAfter('2025-01-01')
    .sortByStartDate('desc')
    .build(),
);

// --- Domain-specific sub-resource helper ---
const equipmentLines = await rentman.projects.listEquipment(projects[0].id);

// --- Equipment query builder ---
const activeEquipment = await rentman.equipment.listAll(
  equipmentQuery().notArchived().sortByName().build(),
);

console.log(`Projects: ${projects.length}, active equipment: ${activeEquipment.length}`);

OOP interface (consumer API)

The supported way to use this package is through the domain-level OOP facade — no ENDPOINTS.* constants, raw path strings, or manual RentmanQueryOptions construction required in your code.

import {
  createRentmanClient,
  projectQuery,
  equipmentQuery,
  contactQuery,
  invoiceQuery,
} from '@alternative-design-and-media/rentman-api-connector';

const rentman = createRentmanClient({ token: process.env.RENTMAN_TOKEN! });

// 1. List confirmed projects starting after a given date
const projects = await rentman.projects.listAll(
  projectQuery()
    .startingAfter('2025-01-01')
    .withStatus('/statuses/3')
    .sortByStartDate('desc')
    .build(),
);

// 2. Equipment lines on the first project
const gear = await rentman.projects.listEquipment(projects[0].id);

// 3. Create a new contact
const { data: contact } = await rentman.contacts.create({
  displayname: 'Teszt Kft.',
  email: '[email protected]',
});

// 4. All active equipment, sorted by name
const activeGear = await rentman.equipment.listAll(
  equipmentQuery().notArchived().sortByName().build(),
);

// 5. Invoice lines for a specific invoice
const lines = await rentman.invoices.listLines(42);

Domain facades at a glance

| Property | Type | Extra methods | |---|---|---| | rentman.projects | ResourceApi<RentmanProject> | listEquipment, listCrew, listFunctions, listVehicles | | rentman.subProjects | ResourceApi<RentmanSubProject> | — | | rentman.contacts | ResourceApi<RentmanContact> | — | | rentman.contactPersons | ResourceApi<RentmanContactPerson> | — | | rentman.equipment | ResourceApi<RentmanEquipmentItem> | listSetContents | | rentman.invoices | ResourceApi<RentmanInvoice> | listLines, listMoments | | rentman.quotes | ResourceApi<RentmanQuote> | listLines | | rentman.crew | CrewResourceApi | — | | rentman.crewAvailabilities | ResourceApi<RentmanCrewAvailability> | — | | rentman.crewRates | ResourceApi<RentmanCrewRate> | — | | rentman.vehicles | ResourceApi<RentmanVehicle> | — | | rentman.payments | ResourceApi<RentmanPayment> | — | | rentman.appointments | ResourceApi<RentmanAppointment> | listCrew | | rentman.subrentals | ResourceApi<RentmanSubrental> | listEquipment, listEquipmentGroups | | rentman.files | ResourceApi<RentmanFile> | — | | rentman.fileFolders | ResourceApi<RentmanFileFolder> | — | | rentman.folders | ResourceApi<RentmanFolder> | — | | rentman.contracts | ResourceApi<RentmanContract> | — | | rentman.costs | ResourceApi<RentmanCost> | — | | rentman.stockMovements | ResourceApi<RentmanStockMovement> | — | | rentman.stockLocations | ResourceApi<RentmanStockLocation> | — | | rentman.timeRegistrations | ResourceApi<RentmanTimeRegistration> | — | | rentman.timeRegistrationActivities | ResourceApi<RentmanTimeRegistrationActivity> | — | | rentman.leaveMutations | ResourceApi<RentmanLeaveMutation> | — | | rentman.leaveRequests | ResourceApi<RentmanLeaveRequest> | — | | rentman.leaveTypes | ResourceApi<RentmanLeaveType> | — | | rentman.repairs | ResourceApi<RentmanRepair> | — | | rentman.serialNumbers | ResourceApi<RentmanSerialNumber> | — | | rentman.accessories | ResourceApi<RentmanAccessory> | — | | rentman.rates | ResourceApi<RentmanRate> | — | | rentman.rateFactors | ResourceApi<RentmanRateFactor> | — | | rentman.factorGroups | ResourceApi<RentmanFactorGroup> | — | | rentman.factors | ResourceApi<RentmanFactor> | — | | rentman.projectTypes | ResourceApi<RentmanProjectType> | — | | rentman.statuses | ResourceApi<RentmanStatus> | — | | rentman.taxClasses | ResourceApi<RentmanTaxClass> | — | | rentman.ledgerCodes | ResourceApi<RentmanLedgerCode> | — | | rentman.projectRequests | ResourceApi<RentmanProjectRequest> | — | | rentman.projectRequestEquipment | ResourceApi<RentmanProjectRequestEquipment> | — | | rentman.actualContent | ResourceApi<RentmanActualContent> | — | | rentman.equipmentAssignedSerials | ResourceApi<RentmanEquipmentAssignedSerial> | — |

Every ResourceApi<T> exposes:

interface ResourceApi<T, TCreate = Partial<T>> {
  list(query?: RentmanQueryOptions): Promise<RentmanCollectionResponse<T>>;
  listAll(query?: Omit<RentmanQueryOptions, 'limit' | 'offset'>): Promise<T[]>;
  getById(id: number, query?: Pick<RentmanQueryOptions, 'fields'>): Promise<RentmanItemResponse<T>>;
  create(body: TCreate): Promise<RentmanItemResponse<T>>;
  update(id: number, body: TCreate): Promise<RentmanItemResponse<T>>;
  delete(id: number): Promise<void>;
}

Typed query builders

Use the typed query builder helpers to compose RentmanQueryOptions without touching the raw type:

| Builder | Factory | Domain-specific methods | |---|---|---| | ProjectQueryBuilder | projectQuery() | startingAfter(date), startingBefore(date), withStatus(path), forCustomer(path), forCustomerId(id), forProjectType(id), notArchived(), inFolder(path), sortByStartDate(dir?), sortByName(dir?) | | EquipmentQueryBuilder | equipmentQuery() | notArchived(), inFolder(path), sortByName(dir?) | | ContactQueryBuilder | contactQuery() | inCountry(code), notArchived(), sortByName(dir?) | | InvoiceQueryBuilder | invoiceQuery() | withStatus(path), forContact(path), dueBefore(date), dueAfter(date), sortByDate(dir?), sortByDueDate(dir?) |

All builders also inherit fields(...), sort(...), limit(n), offset(n), and .build().

⚠️ Deprecated: low-level direct API-call methods (client.list, client.listAll, client.listSub, client.listAllSub, client.get, client.create, client.update, client.delete) are kept only for migration and are no longer supported for new implementations.


Implemented API Connectors

All 52 top-level collection endpoints from OAS v1.7.0 are available as typed constants in ENDPOINTS.

Equipment & Inventory

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | equipment | /equipment | RentmanEquipmentItem | Equipment items (gear, props, sets) | | equipmentSetsContent | /equipmentsetscontent | RentmanEquipmentSetContent | Kit / set components | | actualContent | /actualcontent | RentmanActualContent | Actual serial content of combinations | | equipmentAssignedSerials | /equipmentassignedserials | RentmanEquipmentAssignedSerial | Assigned serial links for combinations | | accessories | /accessories | RentmanAccessory | Equipment accessories | | stockMovements | /stockmovements | RentmanStockMovement | Inventory in/out movements | | stockLocations | /stocklocations | RentmanStockLocation | Warehouse stock locations | | serialNumbers | /serialnumbers | RentmanSerialNumber | Serial-tracked equipment | | repairs | /repairs | RentmanRepair | Repair / service records |

Contacts & People

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | contacts | /contacts | RentmanContact | Companies and persons | | contactPersons | /contactpersons | RentmanContactPerson | Persons linked to a contact | | crew | /crew | RentmanCrewMember | Crew / staff members | | crewAvailabilities | /crewavailability | RentmanCrewAvailability | Crew availability windows | | crewRates | /crewrates | RentmanCrewRate | Crew rate assignments |

Projects

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | projects | /projects | RentmanProject | Top-level projects | | subProjects | /subprojects | RentmanSubProject | Sub-projects / sections | | projectEquipment | /projectequipment | RentmanProjectEquipment | Equipment lines on a project | | projectEquipmentGroups | /projectequipmentgroup | RentmanProjectEquipmentGroup | Equipment line groups | | projectFunctions | /projectfunctions | RentmanProjectFunction | Crew function lines on a project | | projectFunctionGroups | /projectfunctiongroups | RentmanProjectFunctionGroup | Function line groups | | projectCrew | /projectcrew | RentmanProjectCrew | Crew assignments on a project | | projectVehicles | /projectvehicles | RentmanProjectVehicle | Vehicle assignments on a project | | projectRequests | /projectrequests | RentmanProjectRequest | Incoming project requests | | projectRequestEquipment | /projectrequestequipment | RentmanProjectRequestEquipment | Equipment linked to project requests | | projectTypes | /projecttypes | RentmanProjectType | Project type lookup values |

Finance

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | invoices | /invoices | RentmanInvoice | Invoices | | invoiceLines | /invoicelines | RentmanInvoiceLine | Individual invoice lines | | quotes | /quotes | RentmanQuote | Quotes / offers | | payments | /payments | RentmanPayment | Payments against invoices | | costs | /costs | RentmanCost | Additional costs on a project | | taxClasses | /taxclasses | RentmanTaxClass | VAT / tax class definitions | | ledgerCodes | /ledgercodes | RentmanLedgerCode | Accounting ledger codes | | rates | /rates | RentmanRate | Rate / pricing rules | | rateFactors | /ratefactors | RentmanRateFactor | Crew rate factor ranges | | factors | /factors | RentmanFactor | Pricing factors | | factorGroups | /factorgroups | RentmanFactorGroup | Factor groups |

Subrentals

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | subrentals | /subrentals | RentmanSubrental | Subrental orders | | subrentalEquipment | /subrentalequipment | RentmanSubrentalEquipment | Equipment lines on a subrental | | subrentalEquipmentGroups | /subrentalequipmentgroup | RentmanSubrentalEquipmentGroup | Equipment groups on a subrental |

Appointments & Vehicles

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | appointments | /appointments | RentmanAppointment | Calendar appointments | | appointmentCrew | /appointmentcrew | RentmanAppointmentCrew | Crew on an appointment | | vehicles | /vehicles | RentmanVehicle | Vehicle records |

Time & Leave

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | timeRegistrations | /timeregistration | RentmanTimeRegistration | Time registration records | | timeRegistrationActivities | /timeregistrationactivities | RentmanTimeRegistrationActivity | Time registration activity types | | leaveMutations | /leavemutation | RentmanLeaveMutation | Leave balance mutations | | leaveRequests | /leaverequest | RentmanLeaveRequest | Leave requests from crew | | leaveTypes | /leavetypes | RentmanLeaveType | Leave type definitions |

Files & Folders

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | files | /files | RentmanFile | File attachments | | fileFolders | /file_folders | RentmanFileFolder | File folders | | folders | /folders | RentmanFolder | General folder structure |

Reference / Lookup Tables

| ENDPOINTS key | API path | Type | Description | |---|---|---|---| | statuses | /statuses | RentmanStatus | Status lookup values | | contracts | /contracts | RentmanContract | Contracts on a project |

Known TypeScript ↔ OAS field-name mappings

These legacy aliases are intentionally preserved for backward compatibility:

| TypeScript field | OAS field | Entity | Note | |---|---|---|---| | postcode | postal_code | RentmanCrewMember | Legacy abbreviation | | middle | middle_name | RentmanCrewMember | Simplified name | | surname | lastname | RentmanCrewMember | Terminology difference | | tag | tags | RentmanCrewMember | Singular vs plural | | contact | customer / cust_contact | RentmanProject, RentmanInvoice | OAS uses two related fields | | projecttype | project_type | RentmanProject | Legacy casing mismatch | | due_date | expiration | RentmanInvoice | Different legacy semantic naming |


Detailed Usage Examples (legacy low-level API, deprecated)

⚠️ This section documents the old direct endpoint-based API for migration only. New implementations should use the OOP facade (rentman.projects, rentman.equipment, rentman.contacts, ...).

Listing with field selection and sorting

Only request the fields you need to keep responses lean. Pass fields as an array of field names; the API returns only those keys.

import { createRentmanClient, ENDPOINTS, type RentmanEquipmentItem } from '@alternative-design-and-media/rentman-api-connector';

const rentman = createRentmanClient({ token: process.env.RENTMAN_TOKEN! });

const { data, itemCount } = await rentman.list<RentmanEquipmentItem>(ENDPOINTS.equipment, {
  fields: ['id', 'name', 'code', 'in_quantity', 'current_quantity', 'location_in_warehouse'],
  sort: ['+name'],          // prefix '+' = ascending, '-' = descending
  limit: 100,
  offset: 0,
});

for (const item of data) {
  console.log(`[${item.code}] ${item.name} — stock: ${item.current_quantity}`);
}

Auto-pagination (listAll)

Collection requests are now cursor-based by default. Rentman still defaults to 300 items per page, but now allows up to 1500. listAll follows next_page_url automatically and falls back to offset pagination only when the API omits next_page_url (for example when sorting by a non-id field).

const allProjects = await rentman.listAll<RentmanProject>(ENDPOINTS.projects, {
  fields: ['id', 'number', 'name', 'status', 'planperiod_start', 'planperiod_end'],
  sort: ['-planperiod_start'],
});

console.log(`Total projects: ${allProjects.length}`);

Filtering

Exact-match filter (filters)

Pass key/value pairs to filter by equality. Multiple pairs are ANDed together.

const ukContacts = await rentman.list<RentmanContact>(ENDPOINTS.contacts, {
  filters: { country: 'gb' },
  fields: ['id', 'displayname', 'email'],
});

Relational filter (relFilters)

Use rel(field, operator, value) for comparisons. Supported operators: gt, gte, lt, lte, neq.

import { rel } from '@alternative-design-and-media/rentman-api-connector';

// Equipment items with a replacement cost greater than 500
const expensive = await rentman.list<RentmanEquipmentItem>(ENDPOINTS.equipment, {
  relFilters: [rel('replacement_cost', 'gt', 500)],
  sort: ['-replacement_cost'],
});

Null / not-null filter (nullFilters)

Use notNull(field) or isNull(field) to check for the presence or absence of a value.

import { notNull, isNull } from '@alternative-design-and-media/rentman-api-connector';

// Projects that have a contact assigned and are not archived
const activeProjects = await rentman.list<RentmanProject>(ENDPOINTS.projects, {
  nullFilters: [notNull('contact')],
  filters: { status: 'confirmed' },
});

// Equipment without a folder (unorganised items)
const unorganised = await rentman.list<RentmanEquipmentItem>(ENDPOINTS.equipment, {
  nullFilters: [isNull('folder')],
});

Combining all filter types

const result = await rentman.list<RentmanEquipmentItem>(ENDPOINTS.equipment, {
  filters: { type: 'set' },
  relFilters: [rel('in_quantity', 'gte', 1)],
  nullFilters: [notNull('folder'), notNull('code')],
  sort: ['+name'],
  fields: ['id', 'name', 'code', 'in_quantity'],
  limit: 50,
});

Getting a single item

const { data: project } = await rentman.get<RentmanProject>(ENDPOINTS.projects, 1234);

console.log(project.name);          // typed string
console.log(project.planperiod_start); // typed string | null
console.log(project.updateHash);    // use to detect changes

Creating a resource

const { data: newMovement } = await rentman.create<Partial<RentmanStockMovement>, RentmanStockMovement>(
  ENDPOINTS.stockMovements,
  {
    equipment: '/equipment/42',
    quantity: 10,
    type: 'manual',
    date: new Date().toISOString(),
    remark: 'Received from supplier',
  }
);
console.log(`Created movement id: ${newMovement.id}`);

Updating a resource

Only send the fields you want to change. The connector issues a PUT request with your partial payload.

await rentman.update(ENDPOINTS.equipment, 42, {
  remark: 'Calibrated 2025-01',
  location_in_warehouse: 'Shelf A3',
});

Deleting a resource

await rentman.delete(ENDPOINTS.equipment, 99);

Token rotation (zero-downtime)

Pass an async factory instead of a static string. The client calls it before every request.

const rentman = createRentmanClient({
  token: async () => {
    // Fetch a fresh JWT from your vault / KV store
    return await getTokenFromKV('rentman_token');
  },
});

Custom Fields

Rentman supports account-specific custom fields on many entities. They are returned by the API as custom_<number> keys inside the custom object (e.g. custom_16, custom_57).

Default behaviour (open type)

By default the custom field is typed as Partial<Record<\custom_${number}`, string | number | boolean | null>>`. This keeps backward compatibility but loses autocomplete for specific keys.

const { data: item } = await rentman.get<RentmanEquipmentItem>(ENDPOINTS.equipment, 42);
// custom is typed as Partial<Record<`custom_${number}`, ...>>
console.log(item.custom?.custom_16); // works, but no autocomplete for the key name

Narrowing custom fields (recommended)

Define an interface with the exact keys used in your Rentman account and pass it as the TCustom type argument. You get full autocomplete and compile-time safety.

import {
  createRentmanClient,
  ENDPOINTS,
  type RentmanEquipmentItem,
} from '@alternative-design-and-media/rentman-api-connector';

// Define YOUR account's custom fields for equipment
interface EquipmentCustom {
  custom_16?: string;  // "Name EN" — English description
  custom_57?: string;  // "Safety ID" — safety certification code
  custom_88?: number;  // "Weight net (kg)"
}

// Apply the type argument
type MyEquipmentItem = RentmanEquipmentItem<EquipmentCustom>;

const rentman = createRentmanClient({ token: process.env.RENTMAN_TOKEN! });

const { data: item } = await rentman.get<MyEquipmentItem>(ENDPOINTS.equipment, 42);

// Now fully typed and autocompleted:
console.log(item.custom?.custom_16); // string | undefined
console.log(item.custom?.custom_57); // string | undefined
console.log(item.custom?.custom_88); // number | undefined

Custom fields on other entities

The same pattern works on any entity that extends RentmanBaseEntityWithCustom:

| Type | Supports TCustom | |---|---| | RentmanEquipmentItem | ✅ | | RentmanContact | ✅ | | RentmanContactPerson | ✅ | | RentmanCrewMember | ✅ | | RentmanProject | ✅ | | RentmanSubProject | ✅ | | RentmanVehicle | ✅ | | RentmanSubrental | ✅ | | RentmanSubrentalEquipment | ✅ | | RentmanTimeRegistration | ✅ | | RentmanRepair | ✅ | | RentmanSerialNumber | ✅ |

Example: typed custom fields on a project

interface ProjectCustom {
  custom_3?: string;   // "PO Number"
  custom_11?: boolean; // "Requires insurance"
  custom_24?: number;  // "Estimated crew hours"
}

type MyProject = RentmanProject<ProjectCustom>;

const { data: project } = await rentman.get<MyProject>(ENDPOINTS.projects, 500);

if (project.custom?.custom_11) {
  console.log(`Project ${project.name} requires insurance. PO: ${project.custom.custom_3}`);
}

Accessing unknown / unmapped fields

If you need to access a field that is not yet in the type definitions (e.g. after a Rentman API update), use WithUnknownFields<T>:

import { type WithUnknownFields, type RentmanEquipmentItem } from '@alternative-design-and-media/rentman-api-connector';

const { data: raw } = await rentman.get<WithUnknownFields<RentmanEquipmentItem>>(ENDPOINTS.equipment, 42);
console.log(raw.someNewUnmappedField); // typed as `unknown`, no compile error

Custom fields + OOP facade (Prisma-style DX)

Use createTypedClient to integrate account-specific custom fields directly into the OOP facade — no low-level API calls needed. The pattern is inspired by Prisma's prisma generate / graphql-codegen workflow.

Step 1 — Define your custom fields config

// custom-fields.config.json
[
  { "id": 11, "name": "budget", "belongs_to": "project", "type": "price", "input_fields_group": "General", "required": false },
  { "id": 12, "name": "category", "belongs_to": "project", "type": "dropdown", "input_fields_group": "General", "required": true,
    "options": [{ "id": 1, "name": "Conference" }, { "id": 2, "name": "Wedding" }] },
  { "id": 21, "name": "serial_prefix", "belongs_to": "equipment", "type": "text", "input_fields_group": "General", "required": false }
]

Step 2 — Regenerate types (run once per config change)

npx generate-rentman-custom-fields
# or inside your project:
npm run generate:custom-fields

This writes src/generated/custom-fields.generated.ts which includes a RentmanCustomFields extends CustomFieldMap interface alongside per-model helpers.

Step 3 — Create the typed client (written once by hand)

// src/lib/rentman.ts
import {
  createRentmanClient,
  createTypedClient,
} from '@alternative-design-and-media/rentman-api-connector';
import type { RentmanCustomFields } from './generated/custom-fields.generated';

const base = createRentmanClient({ token: process.env.RENTMAN_TOKEN! });

// The second argument is only used for TypeScript inference.
// {} as RentmanCustomFields is sufficient at runtime.
export const rentman = createTypedClient(base, {} as RentmanCustomFields);

Step 4 — Use in your application code

import { rentman } from './lib/rentman';

const projects = await rentman.projects.listAll();
projects[0].custom?.budget;   // ✅ number
projects[0].custom?.category; // ✅ 'Conference' | 'Wedding'

const items = await rentman.equipment.listAll();
items[0].custom?.serial_prefix; // ✅ string | undefined

const crew = await rentman.crew.listAll();
crew[0].custom?.has_driving_license; // ✅ boolean

const contactPersons = await rentman.contactPersons.listAll();
// Requires a `contactPersons` schema in your generated CustomFieldMap.
contactPersons[0].custom?.has_signing_authority; // ✅ boolean

// Deprecated low-level API (migration only)
const raw = await rentman.list(ENDPOINTS.projects);

// Sub-resource methods are preserved on the typed client
const equipment = await rentman.projects.listEquipment(123);

Entities without custom fields in your CustomFieldMap fall back to Record<string, never> (typed, but no false positives). Backward compatible: createRentmanClient without createTypedClient continues to work unchanged.


Resource Path Utilities

Rentman resource references are path strings like "/contacts/123". These helpers let you parse, extract, and build those paths in a null-safe, type-safe way.

parseResourcePath(path)

Parses a path into its entity name and numeric ID. Returns null for any invalid or missing input.

import { parseResourcePath } from '@alternative-design-and-media/rentman-api-connector';

parseResourcePath('/contacts/123') // → { entity: 'contacts', id: 123 }
parseResourcePath('/equipment/42') // → { entity: 'equipment', id: 42 }
parseResourcePath(null)            // → null
parseResourcePath('/contacts')     // → null  (no ID segment)
parseResourcePath('/contacts/abc') // → null  (non-numeric ID)

resourceId(path)

Extracts only the numeric ID from a path. Returns null for any invalid or missing input.

import { resourceId } from '@alternative-design-and-media/rentman-api-connector';

resourceId('/folders/42')  // → 42
resourceId('/contacts/0')  // → 0
resourceId(null)           // → null
resourceId('/contacts')    // → null

buildResourcePath(endpoint, id)

Builds a canonical Rentman resource path from a typed ENDPOINTS constant and a numeric ID.

import { buildResourcePath, ENDPOINTS } from '@alternative-design-and-media/rentman-api-connector';

buildResourcePath(ENDPOINTS.contacts, 123)  // → '/contacts/123'
buildResourcePath(ENDPOINTS.equipment, 42)  // → '/equipment/42'

// Use the result directly as a field value in create/update payloads:
await rentman.create(ENDPOINTS.stockMovements, {
  equipment: buildResourcePath(ENDPOINTS.equipment, 42),
  quantity: 10,
  type: 'manual',
});

Lookup Cache Helpers

These helpers fetch an entire reference endpoint and build a Map for fast lookups. They are useful for resolving Rentman resource paths (e.g. "/statuses/3") to display names without repeated API calls.

fetchLookupMap<T, V>(client, endpoint, query, toEntry)

Generic helper that fetches all pages of any endpoint and builds a Map using a caller-supplied key/value extractor. First-wins when toEntry returns duplicate keys.

import {
  createRentmanClient,
  ENDPOINTS,
  fetchLookupMap,
  type RentmanTaxClass,
} from '@alternative-design-and-media/rentman-api-connector';

const rentman = createRentmanClient({ token: process.env.RENTMAN_TOKEN! });

// Build an id → name map for tax classes
const taxMap = await fetchLookupMap<RentmanTaxClass, string>(
  rentman,
  ENDPOINTS.taxClasses,
  {},
  (tc) => [String(tc.id), tc.name],
);

console.log(taxMap.get('1')); // e.g. "21%"

fetchStatusCache(client)

Fetches all Rentman statuses and returns two Maps for bidirectional lookups:

  • byName — lowercase status name → resource path (e.g. "confirmed""/statuses/3")
  • byPath — resource path → display name (e.g. "/statuses/3""Confirmed")
import {
  createRentmanClient,
  fetchStatusCache,
} from '@alternative-design-and-media/rentman-api-connector';

const rentman = createRentmanClient({ token: process.env.RENTMAN_TOKEN! });

const { byName, byPath } = await fetchStatusCache(rentman);

const path  = byName.get('confirmed');     // "/statuses/3"
const label = byPath.get('/statuses/3');   // "Confirmed"

fetchFolderNameCache(client)

Fetches all equipment folders and returns a Map of resource path → folder name.

import {
  createRentmanClient,
  fetchFolderNameCache,
} from '@alternative-design-and-media/rentman-api-connector';

const rentman = createRentmanClient({ token: process.env.RENTMAN_TOKEN! });

const folderNames = await fetchFolderNameCache(rentman);

console.log(folderNames.get('/folders/116')); // "Lighting"

API Reference

createRentmanClient(options)

| Option | Type | Required | Description | |---|---|---|---| | token | string \| () => string \| Promise<string> | ✅ | JWT token or async factory. Using a function enables zero-downtime token rotation. | | baseUrl | string | — | Override the API base URL (default: https://api.rentman.net). Useful for testing. | | fetch | typeof fetch | — | Custom fetch implementation. Defaults to globalThis.fetch. |

Static token values may be provided either as a bare JWT or as a "Bearer ..." string. The client normalizes the prefix internally.

client.list<T>(path, query?) (deprecated)

Fetch a collection. Returns RentmanCollectionResponse<T> with data, itemCount, limit, offset, and optional next_page_url.

client.listAll<T>(path, query?, pageSize?) (deprecated)

Auto-paginate through all items. pageSize defaults to 1500 (the Rentman API max), while the API itself still defaults to 300 when no limit is sent.

listWithPreservedSlashes<T>(client, endpoint, query, options?) (deprecated)

Fetch a collection while keeping / characters unescaped in resource-path filter values such as equipment[eq]=/equipment/4362.

scanAll<T>(client, endpoint, query, options?) (deprecated)

Paginated scan helper that returns { items, totalCount, limitReached }.

import { ENDPOINTS, scanAll } from '@alternative-design-and-media/rentman-api-connector';

const { items, limitReached, totalCount } = await scanAll(rentman, ENDPOINTS.subProjects, {
  sort: ['modified'],
}, {
  pageSize: 300,
  scanLimit: 1500,
});

normalizeToken(token)

Strips an optional "Bearer " prefix from a token string.

import { normalizeToken } from '@alternative-design-and-media/rentman-api-connector';

const token = normalizeToken('Bearer eyJhbGciOi...');

client.listSub<T>(parentPath, parentId, subPath, query?) (deprecated)

Fetch a sub-resource collection via path-level URL generation (${parentPath}/${parentId}${subPath}).

const { data } = await rentman.listSub(
  ENDPOINTS.equipment,
  3473,
  '/equipmentsetscontent',
);

client.listAllSub<T>(parentPath, parentId, subPath, query?, pageSize?) (deprecated)

Backward-compatible sub-resource list helper. When query.limit is omitted, auto-paginates through all items starting from query.offset (defaults to 0) and returns a flat array. When query.limit is provided, returns only that single page's data array without auto-paginating. Use the explicit *Paged methods when page metadata (e.g. itemCount) is required.

listEquipmentSetContents(client, kitId) (deprecated)

Backward-compatible wrapper around client.equipment.listSetContents(kitId).

import {
  listEquipmentSetContents,
  type RentmanEquipmentSetContent,
} from '@alternative-design-and-media/rentman-api-connector';

const contents: RentmanEquipmentSetContent[] = await listEquipmentSetContents(rentman, 3473);

client.get<T>(path, id, query?) (deprecated)

Fetch a single item by numeric ID.

client.create<TIn, TOut>(path, body) (deprecated)

POST a new item.

client.update<TIn, TOut>(path, id, body) (deprecated)

PUT an updated item.

client.delete(path, id) (deprecated)

DELETE an item by ID.

normalizeEquipmentItem(item)

Normalizes mixed Rentman equipment payloads into a single predictable shape. It merges OAS fields (for example current_quantity) with legacy aliases (for example currentquantity), and also keeps the raw item in _raw.

import {
  normalizeEquipmentItem,
  type RentmanEquipmentItem,
} from '@alternative-design-and-media/rentman-api-connector';

const { data: rawItem } = await rentman.equipment.getById(42);
const normalized = normalizeEquipmentItem(rawItem);

console.log(normalized.currentQuantity, normalized.locationInWarehouse);

Query builder helpers

import {
  rel,
  notNull,
  isNull,
  buildQueryParams,
  buildQueryString,
  buildRentmanQuery,
} from '@alternative-design-and-media/rentman-api-connector';

// rel(field, op, value) — relational filter
rel('distance', 'lte', 300)    // → distance[lte]=300

// notNull(field) / isNull(field) — null-check filter
notNull('folder')              // → folder[isnull]=false
isNull('archive')              // → archive[isnull]=true

// buildQueryParams(options) — returns a plain Record<string, string>
const queryParams = buildQueryParams({
  filters: { 'in_archive[eq]': false },
});
// → { 'in_archive[eq]': '0' }

// buildQueryString(options, { preserveSlashes: true }) — keeps path filter slashes readable
const queryString = buildQueryString(
  { filters: { 'status[eq]': '/statuses/3' } },
  { preserveSlashes: true },
);
// → ?status%5Beq%5D=/statuses/3

// buildRentmanQuery(options) — returns URLSearchParams
const params = buildRentmanQuery({
  fields: ['id', 'name'],
  sort: ['+name'],
  filters: { country: 'gb' },
  relFilters: [rel('distance', 'lte', 300)],
  nullFilters: [notNull('folder')],
  limit: 50,
  offset: 0,
});

Preserving slashes in resource-path filters

Some Rentman collection filters expect a resource path value such as /equipment/4362. In these cases, %2F-encoding the slash can break Rentman’s parser. Use listWithPreservedSlashes() for collection requests that need this encoding behavior.

import {
  ENDPOINTS,
  listWithPreservedSlashes,
  type RentmanEquipmentSetContent,
} from '@alternative-design-and-media/rentman-api-connector';

const query = {
  filters: { 'equipment[eq]': '/equipment/4362' },
};

const { data } = await listWithPreservedSlashes<RentmanEquipmentSetContent>(
  rentman,
  ENDPOINTS.equipmentSetsContent,
  query,
  { limit: 100, offset: 0 },
);

listWithPreservedSlashes() still percent-encodes other reserved characters in values; it only keeps / readable for resource-path filters.


OAS Alignment

| OAS version | Deployment date | Synced | |---|---|---| | 1.7.0 | 2025-11-13 | ✅ |

Conventions

  • Field names follow the OAS schema (mostly snake_case, matching Rentman's API).
  • Generated fields (marked in the OAS as GENERATED FIELD) are annotated with a JSDoc comment on the type. They cannot be sorted on when limit/offset are set, and cannot be used as filter keys.
  • updateHash is present on every entity. Use it to detect changes cheaply without comparing all fields.
  • custom exposes the custom_<number> keys returned by the API. Narrow the type at compile time by passing a TCustom type argument (see Custom Fields).
  • Pagination: collection responses are cursor-based by default. The API default page size is 300, the max is 1500, and listAll() / listAllSub() follow next_page_url automatically.
  • Rate limits: 50 000 requests/day, 10 req/s, max 20 concurrent requests.

Contributing

Pull requests are welcome. When adding a new resource type:

  1. Add the interface to src/types.ts, extending RentmanBaseEntity (or RentmanBaseEntityWithCustom<TCustom> if the entity has custom fields).
  2. Annotate any GENERATED FIELD with a JSDoc comment.
  3. Add the endpoint path to src/endpoints.ts.
  4. Run npm test before submitting.

License

MIT © Alternative Design and Media