A comprehensive TypeScript client library for the UISP (Ubiquiti ISP Platform) CRM API. This library provides type-safe access to all UISP CRM endpoints with full TypeScript support.
Version 2.x requires Node.js 22 or newer and uses the native Node fetch implementation.
Version 2.1.0 adds the missing Payments API, fixes the invoice update contract to match what UCRM actually accepts, and surfaces detailed validation errors. Fully backwards-compatible at the import level β the only behavioral change is that client.invoices.updateInvoice now produces requests UCRM accepts instead of being rejected with 422 "This field is not allowed".
- Payments API (
client.payments): full CRUD for/payments/*βgetPayments,getPayment,createPayment,updatePayment,deletePayment,sendReceipt,attach,detach. Companion types:Payment,PaymentReadOnly,PaymentWritable,PaymentUpdate,PaymentMatch,PaymentAttribute,PaymentAttributeReadOnly,PaymentCoverReadOnly,PaymentSearchParams. - Invoice update fix:
InvoiceUpdateandInvoiceNewnow match the UCRM API Blueprint:attributes: InvoiceAttribute[]replaces the (incorrect)customAttributes: Record<string, unknown>field. Earlier versions sent a shape UCRM rejected with HTTP 422.InvoiceUpdate.numberis now writable (used to approve drafts and to override invoice numbers from external integrations).InvoiceNew.items(required) andInvoiceUpdate.items(optional) replace the misnamedinvoiceItems.InvoiceNewaddsapplyCredit?anddraft?per spec.
- Detailed validation errors:
UispValidationError.fieldsexposes a flattened{ "field.path": ["msg", ...] }map of UCRM'serrors.fieldspayload. TheError.messagealready includes the per-field summary so logs and user-facing handlers carry actionable detail (instead of just "Validation failed"). - Raw error body: every
UispCrmError(and 4xx/5xx subclasses) now carries.bodywith the parsed JSON or text returned by UCRM, plus a typedUcrmErrorBodyinterface. - New types:
InvoiceAttribute,InvoiceAttributeReadOnly,UcrmErrorBody.
If you were calling client.invoices.updateInvoice(id, { customAttributes: {...} }) and getting validation errors, replace it with the array shape and use customAttributesApi to resolve key β id:
const attrs = await client.customAttributes.getCustomAttributes();
const idByKey = new Map(
attrs.data.filter((a) => a.attributeType === "invoice").map((a) => [a.key, a.id])
);
await client.invoices.updateInvoice(id, {
number: "00999-00000004", // newly writable
attributes: [
{ customAttributeId: idByKey.get("cae")!, value: "86180063062416" },
{ customAttributeId: idByKey.get("tipofactura")!, value: "A" },
],
});To inspect validation failures with field-level detail:
import { UispValidationError } from "uisp-crm-api";
try {
await client.invoices.updateInvoice(id, payload);
} catch (e) {
if (e instanceof UispValidationError) {
console.log(e.message); // includes per-field summary
console.log(e.fields); // { "attributes.tipofactura": ["Invalid choice"] }
console.log(e.body); // raw response body from UCRM
}
}Version 2.0.0 is a breaking release focused on modernizing the transport layer, tightening the runtime contract, and cleaning up the public configuration surface.
- Native Node fetch: the internal HTTP client now uses the built-in Node fetch API instead of axios.
- Node.js 22+ only: the package now declares
node >=22and should be run on modern Node runtimes with native fetch support. - Axios removed: axios is no longer part of the library requirements.
- Enhanced error handling: requests now use custom UISP error classes for authentication, permission, validation, network, rate-limit, and server failures.
- Production-safe errors: stack traces are hidden outside development mode and error objects carry structured codes and status information.
- Simpler config surface: the undocumented no-op
retriesoption has been removed fromUispCrmConfigandRequestConfig. - Timeout support preserved: request timeouts are still supported through the
timeoutoption, now implemented withAbortController. - File downloads preserved: PDF and document download helpers still return
ArrayBuffer. - Error handling preserved: common HTTP error mappings such as 401, 403, 404, and 422 still return the same library-level error messages.
- Test coverage improved: fetch-mocked tests were added for the HTTP client and representative API request routing.
- Dependency tree cleaned up: the lockfile was refreshed and vulnerable transitive dev dependencies were updated.
- Upgrade your runtime to Node.js 22 or newer before moving to 2.x.
- Remove any expectation that axios is available or used internally by this package.
- Remove
retriesfrom your client configuration if you were passing it. - No changes are required to the main client API shape for normal
clients,services,invoices, and related endpoint usage.
Example migration:
const client = new UispCrmClient({
baseUrl: process.env.UISP_BASE_URL!,
appKey: process.env.UISP_APP_KEY!,
timeout: 30000,
});- π Type-safe - Full TypeScript support with comprehensive type definitions
- π Complete API coverage - All UISP CRM endpoints supported including Services API
- π‘οΈ Enhanced Error handling - Production-safe error handling with custom error types and secure logging
- π Organized structure - Clean, modular API organized by functionality
- π§ Easy configuration - Simple setup with sensible defaults
- π Well documented - Comprehensive documentation and examples
- π Services Management - Full support for service lifecycle management, traffic shaping, and usage tracking
- π Security-focused - Stack traces and sensitive information hidden in production
- π Error tracking - Unique error IDs for debugging and monitoring
npm install uisp-crm-apiRuntime requirement: Node.js 22 or newer.
import { UispCrmClient } from "uisp-crm-api";
// Initialize the client
const client = new UispCrmClient({
baseUrl: "https://your-uisp-instance.com/crm/api/v1.0",
appKey: "your-app-key-here",
});
// Test the connection
const isConnected = await client.testConnection();
// Get all clients
const clients = await client.clients.getClients();
// Create a new client
const newClient = await client.clients.createClient({
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
});
// Get all services
const services = await client.services.getServices();
// Create a service for the client
const newService = await client.services.createService(newClient.data.id, {
name: "Internet Service",
price: 29.99,
});const client = new UispCrmClient({
baseUrl: "https://your-uisp-instance.com/crm/api/v1.0",
appKey: "your-app-key-here",
});const client = new UispCrmClient({
baseUrl: "https://your-uisp-instance.com/crm/api/v1.0",
appKey: "your-app-key-here",
timeout: 30000, // Request timeout in milliseconds (default: 30000)
});You can also use environment variables:
const client = new UispCrmClient({
baseUrl: process.env.UISP_BASE_URL!,
appKey: process.env.UISP_APP_KEY!,
});This library uses UISP CRM App Keys for authentication. To generate an app key:
- Go to your UISP CRM instance
- Navigate to Settings β Security β App keys
- Generate a new app key with appropriate permissions:
- Read permissions for GET requests
- Write permissions for POST, PUT, PATCH, DELETE requests
The client is organized into logical groups of related functionality:
// Get all clients
const clients = await client.clients.getClients({
limit: 10,
offset: 0,
query: "john",
isArchived: 0,
});
// Get specific client
const client = await client.clients.getClient(123);
// Create new client
const newClient = await client.clients.createClient({
firstName: "John",
lastName: "Doe",
email: "john@example.com",
organizationId: 1,
});
// Update client
const updatedClient = await client.clients.updateClient(123, {
note: "Updated via API",
});
// Archive/restore client
await client.clients.archiveClient(123);
await client.clients.restoreClient(123);
// Manage client tags
await client.clients.addClientTag(123, 456);
await client.clients.removeClientTag(123, 456);
// Send invitation
await client.clients.sendInvitation(123);
// Geocode address
await client.clients.geocodeClient(123);// Get all services
const services = await client.services.getServices({
clientId: 123,
organizationId: 1,
statuses: [1], // Active services only
limit: 10,
});
// Get specific service
const service = await client.services.getService(456);
// Create new service for a client
const newService = await client.services.createService(123, {
name: "Premium Internet Service",
price: 49.99,
servicePlanId: 1,
activeFrom: "2024-01-01",
invoicingPeriodType: 1, // MONTH
invoicingPeriod: 1,
street1: "123 Service Address",
city: "Service City",
zipCode: "12345",
});
// Update service
const updatedService = await client.services.updateService(456, {
price: 59.99,
note: "Price updated",
});
// Service management operations
await client.services.geocodeService(456); // Auto-geocode service address
await client.services.activateQuotedService(456, {
activateDate: "2024-01-01",
setupFeeInvoiceImmediately: true,
});
await client.services.suspendService(456); // Suspend service
await client.services.cancelSuspendService(456); // Resume service
await client.services.endService(456); // End service permanently
// Pause service for a period
await client.services.pauseService(456, {
pauseFrom: "2024-06-01",
pauseTo: "2024-06-30",
});
// Cancel deferred changes
await client.services.cancelDeferredChange(456);
// Traffic shaping controls
await client.services.enableTrafficShapingOverride(456, {
downloadSpeedOverride: 100, // Mbps
uploadSpeedOverride: 50, // Mbps
});
await client.services.disableTrafficShapingOverride(456);
// Get service usage data
const usageData = await client.services.getServiceDataUsage(
456,
"2024-01-01T00:00:00+0000",
);
console.log(
`Download: ${usageData.data.download} ${usageData.data.downloadUnit}`,
);
// Service change requests
const changeRequests = await client.services.getServiceChangeRequests();
const newChangeRequest = await client.services.createServiceChangeRequest({
serviceId: 456,
servicePlanId: 2,
note: "Customer requested upgrade",
});
await client.services.acceptServiceChangeRequest(newChangeRequest.data.id);
// Prepaid service periods (for prepaid services)
const periods = await client.services.getPrepaidServicePeriods({
serviceId: 456,
limit: 10,
});
const newPeriod = await client.services.createPrepaidServicePeriod({
serviceId: 456,
startDate: "2024-01-01",
endDate: "2024-01-31",
price: 49.99,
});// Get client bank accounts
const bankAccounts = await client.clients.getClientBankAccounts(123);
// Create bank account
const newAccount = await client.clients.createClientBankAccount(123, {
name: "Primary Account",
field1: "Account Number",
field2: "Routing Number",
});
// Update bank account
await client.clients.updateClientBankAccount(456, {
name: "Updated Account Name",
});
// Delete bank account
await client.clients.deleteClientBankAccount(456);// Get client contacts
const contacts = await client.clients.getClientContacts(123);
// Create contact
const newContact = await client.clients.createClientContact(123, {
name: "Jane Doe",
email: "jane@example.com",
phone: "+1234567890",
isBilling: true,
});
// Update contact
await client.clients.updateClientContact(456, {
name: "Updated Name",
});
// Delete contact
await client.clients.deleteClientContact(456);// Get invoices
const invoices = await client.invoices.getInvoices({
clientId: 123,
statuses: [1, 2], // Draft, Unpaid
createdDateFrom: "2024-01-01",
createdDateTo: "2024-12-31",
});
// Create invoice
const newInvoice = await client.invoices.createInvoiceForClient(123, {
organizationId: 1,
maturityDays: 30,
notes: "Monthly service fee",
items: [
{
type: "service",
label: "Internet Service",
price: 50.0,
quantity: 1,
unit: "month",
taxRate1: 10,
},
],
});
// Update invoice (since 2.1.0): override `number` and set custom attributes by id.
// Resolve attribute keys β ids via `client.customAttributes.getCustomAttributes()`.
await client.invoices.updateInvoice(456, {
number: "00999-00000004",
attributes: [
{ customAttributeId: 16, value: "86180063062416" },
{ customAttributeId: 18, value: "A" },
],
});
// Invoice operations
await client.invoices.approveInvoice(456);
await client.invoices.sendInvoice(456);
await client.invoices.voidInvoice(456);
// Download PDF
const pdfBuffer = await client.invoices.getInvoicePdf(456);// Get credit notes
const creditNotes = await client.creditNotes.getCreditNotes({
clientId: 123,
createdDateFrom: "2024-01-01",
});
// Create credit note
const newCreditNote = await client.creditNotes.createCreditNoteForClient(123, {
organizationId: 1,
number: "CN-001",
notes: "Service credit",
creditNoteItems: [
{
type: "credit",
label: "Service Credit",
price: -25.0,
quantity: 1,
},
],
});
// Send credit note
await client.creditNotes.sendCreditNote(456);
// Download PDF
const pdfBuffer = await client.creditNotes.getCreditNotePdf(456);// Get organizations
const organizations = await client.organizations.getOrganizations();
// Create organization
const newOrg = await client.organizations.createOrganization({
name: "My ISP Company",
email: "billing@myisp.com",
street1: "123 Main St",
city: "Anytown",
zipCode: "12345",
});
// Get next invoice numbers
const nextInvoice = await client.organizations.getNextInvoiceNumber(1);
const nextProforma = await client.organizations.getNextProformaInvoiceNumber(1);
const nextQuote = await client.organizations.getNextQuoteNumber(1);
// Send email
await client.organizations.enqueueEmail(1, {
subject: "Test Email",
body: "Hello World",
to: ["customer@example.com"],
});// Get payments collection (paginated, filterable)
const payments = await client.payments.getPayments({
clientId: 123,
createdDateFrom: "2024-01-01",
unattached: 1,
limit: 50,
});
// Get a single payment by id (returns paymentCovers + attributes)
const payment = await client.payments.getPayment(460898);
console.log(payment.data.methodId); // UUID of the payment method
console.log(payment.data.paymentCovers); // [{ invoiceId, amount, ... }]
// Create a payment and auto-apply to client's unpaid invoices
const created = await client.payments.createPayment({
clientId: 123,
methodId: "6efe0fa8-36b2-4dd1-b049-427bffc7d369",
amount: 234.0,
currencyCode: "ARS",
note: "Cash payment received in office",
applyToInvoicesAutomatically: true,
});
// Update note + custom attributes on a payment.
// Send `value: null` on an attribute to clear it.
await client.payments.updatePayment(460898, {
note: "Updated note",
attributes: [
{ customAttributeId: 25, value: "REF-2024-0001" },
{ customAttributeId: 26, value: null },
],
});
// Send the receipt email to the client
await client.payments.sendReceipt(460898);
// Attach an unattached payment to a client + invoices
await client.payments.attach(460898, {
clientId: 123,
invoiceIds: [556577],
sendReceipt: true,
});
// Detach a payment from its client
await client.payments.detach(460898);
// Delete a payment
await client.payments.deletePayment(460898);// Get jobs
const jobs = await client.jobs.getJobs({
clientId: 123,
assignedUserId: 456,
dateFrom: "2024-01-01",
dateTo: "2024-12-31",
});
// Create job
const newJob = await client.jobs.createJob({
title: "Installation Service",
description: "Install new service",
clientId: 123,
assignedUserId: 456,
date: "2024-12-01T10:00:00Z",
duration: 120,
status: 1,
});
// Add job comment
await client.jobComments.createJobComment({
jobId: 789,
message: "Job completed successfully",
});
// Add job task
await client.jobTasks.createJobTask({
jobId: 789,
label: "Verify equipment",
closed: false,
});// Get documents
const documents = await client.documents.getDocuments({
clientId: 123,
types: ["document", "image"],
});
// Upload document
const newDoc = await client.documents.createDocument({
clientId: 123,
name: "contract.pdf",
file: base64FileContent,
mimeType: "application/pdf",
});
// Download file
const fileBuffer = await client.documents.getDocumentFile(456);// Get custom attributes
const attributes = await client.customAttributes.getCustomAttributes({
attributeType: "client",
});
// Create custom attribute
const newAttribute = await client.customAttributes.createCustomAttribute({
name: "Customer Tier",
attributeType: "client",
key: "customer_tier",
});// Geocode address
const location = await client.geocoding.geocode({
address: "123 Main St, Anytown, USA",
});
// Get address suggestions
const suggestions = await client.geocoding.suggestAddresses({
query: "123 Main",
lat: "40.7128",
lon: "-74.0060",
});The library provides comprehensive, production-safe error handling with custom error types:
import {
UispNetworkError,
UispAuthenticationError,
UispPermissionError,
UispNotFoundError,
UispValidationError,
} from "uisp-crm-api";
try {
const c = await client.clients.getClient(123);
} catch (error) {
if (error instanceof UispAuthenticationError) {
console.error("Authentication failed - check your app key");
} else if (error instanceof UispPermissionError) {
console.error("Insufficient permissions for this operation");
} else if (error instanceof UispNotFoundError) {
console.error("Resource not found:", error.message);
} else if (error instanceof UispValidationError) {
// Since 2.1.0: `error.message` already includes per-field detail.
// `error.fields` is a flattened map of all field-level errors.
// `error.body` is the raw response body returned by UCRM.
console.error("Validation failed:", error.message);
console.error("Per-field errors:", error.fields);
// e.g. { "attributes.tipofactura": ["Invalid choice"], "number": ["Already in use"] }
} else if (error instanceof UispNetworkError) {
console.error("Network connection failed");
} else {
console.error("Unexpected error:", error.message);
}
}The library includes specific error types for different scenarios:
UispNetworkError- Connection issues, server unreachableUispAuthenticationError- Invalid or missing app key (401)UispPermissionError- Insufficient permissions (403)UispNotFoundError- Resource not found (404)UispValidationError- Request validation failures (422). Carries.fields(flattened per-field errors) and.body(raw response).UispRateLimitError- Rate limiting exceeded (429)UispServerError- Internal server errors (500)UispServiceUnavailableError- Service temporarily unavailable (503)
Since 2.1.0, every UispCrmError exposes a .body field with the raw JSON or text body from UCRM β useful for inspecting error codes or fields the library doesn't surface explicitly.
- π Stack trace protection: Stack traces are hidden in production environments
- π Sensitive data masking: API keys, file paths, and internal details are never exposed
- π Error tracking: Each error includes a unique tracking ID for debugging
- π Structured logging: Clean, consistent error messages suitable for monitoring systems
This library is written in TypeScript and provides full type definitions:
import { ClientReadOnly, ClientWritable, InvoiceReadOnly } from "uisp-crm-api";
// All API responses are properly typed
const clients: ClientReadOnly[] = (await client.clients.getClients()).data;
// Input parameters are validated at compile time
const newClient: ClientWritable = {
firstName: "John", // β
Valid
lastName: "Doe", // β
Valid
invalidField: "value", // β TypeScript error
};See the examples/ directory for more comprehensive examples:
- Basic Usage
- Services API Examples - Complete Services API usage examples
- Advanced scenarios
- Error handling patterns
- Real-world use cases
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
# Install dependencies
npm install
# Build the library
npm run build
# Run tests
npm test
# Lint code
npm run lint
# Watch mode for development
npm run build:watchMIT License - see LICENSE file for details.
- UISP CRM Documentation
- GitHub Issues
- API Blueprint - Original API specification
This project is licensed under the MIT License - see the LICENSE file for details.