/**
* Rippling API client with authentication, pagination, and rate limiting.
*/
import { RipplingApiError, RipplingConfigError } from "../utils/errors.js";
import { RateLimitTracker } from "../utils/rate-limit.js";
export interface RipplingConfig {
apiToken?: string;
baseUrl?: string;
}
export interface PaginationParams {
limit?: number;
offset?: number;
}
export class RipplingClient {
private readonly baseUrl: string;
private readonly apiToken: string;
private readonly rateLimiter = new RateLimitTracker();
constructor(config?: RipplingConfig) {
this.baseUrl =
config?.baseUrl ||
process.env.RIPPLING_BASE_URL ||
"https://api.rippling.com/platform/api";
const token = config?.apiToken || process.env.RIPPLING_API_TOKEN;
if (!token) {
throw new RipplingConfigError(
"RIPPLING_API_TOKEN is required. Set it as an environment variable or pass it in the config."
);
}
this.apiToken = token;
}
private async request<T>(
endpoint: string,
options: {
method?: string;
params?: Record<string, string | number | undefined>;
body?: unknown;
} = {}
): Promise<T> {
await this.rateLimiter.waitIfNeeded();
const { method = "GET", params, body } = options;
const url = new URL(`${this.baseUrl}${endpoint}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
}
const headers: Record<string, string> = {
Authorization: `Bearer ${this.apiToken}`,
Accept: "application/json",
};
if (body) {
headers["Content-Type"] = "application/json";
}
const response = await fetch(url.toString(), {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
this.rateLimiter.updateFromHeaders(response.headers);
if (!response.ok) {
let detail: string | undefined;
try {
const errorBody = await response.text();
detail = errorBody;
} catch {
// ignore parse errors
}
throw new RipplingApiError(
`Rippling API returned ${response.status}`,
response.status,
endpoint,
detail
);
}
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
// ── Company ──────────────────────────────────────────
async getCompany(): Promise<unknown> {
return this.request("/companies/current");
}
async listDepartments(
pagination?: PaginationParams
): Promise<unknown[]> {
return this.request("/departments", {
params: {
limit: pagination?.limit,
offset: pagination?.offset,
},
});
}
async listWorkLocations(): Promise<unknown[]> {
return this.request("/work_locations");
}
async listTeams(): Promise<unknown[]> {
return this.request("/teams");
}
async listLevels(pagination?: PaginationParams): Promise<unknown[]> {
return this.request("/levels", {
params: {
limit: pagination?.limit,
offset: pagination?.offset,
},
});
}
async listCustomFields(
pagination?: PaginationParams
): Promise<unknown[]> {
return this.request("/custom_fields", {
params: {
limit: pagination?.limit,
offset: pagination?.offset,
},
});
}
async getCompanyActivity(params?: {
startDate?: string;
endDate?: string;
limit?: number;
}): Promise<unknown[]> {
return this.request("/company_activity", {
params: {
startDate: params?.startDate,
endDate: params?.endDate,
limit: params?.limit,
},
});
}
// ── Employees ────────────────────────────────────────
async listEmployees(
pagination?: PaginationParams
): Promise<unknown[]> {
return this.request("/employees", {
params: {
limit: pagination?.limit,
offset: pagination?.offset,
},
});
}
async getEmployee(employeeId: string): Promise<unknown> {
return this.request(`/employees/${encodeURIComponent(employeeId)}`);
}
async listAllEmployees(
pagination?: PaginationParams & { ein?: string }
): Promise<unknown[]> {
return this.request("/employees/include_terminated", {
params: {
limit: pagination?.limit,
offset: pagination?.offset,
ein: pagination?.ein,
},
});
}
// ── Leave Management ─────────────────────────────────
async getLeaveBalances(roleId: string): Promise<unknown> {
return this.request(
`/leave_balances/${encodeURIComponent(roleId)}`
);
}
async listLeaveRequests(params?: {
status?: string;
startDate?: string;
endDate?: string;
requestedBy?: string;
limit?: number;
offset?: number;
}): Promise<unknown[]> {
return this.request("/leave_requests", {
params: {
status: params?.status,
startDate: params?.startDate,
endDate: params?.endDate,
requestedBy: params?.requestedBy,
limit: params?.limit,
offset: params?.offset,
},
});
}
async processLeaveRequest(
requestId: string,
action: "APPROVE" | "DECLINE"
): Promise<unknown> {
return this.request(
`/leave_requests/${encodeURIComponent(requestId)}/process`,
{
method: "POST",
body: { status: action === "APPROVE" ? "APPROVED" : "DECLINED" },
}
);
}
async updateLeaveRequest(
requestId: string,
updates: Record<string, unknown>
): Promise<unknown> {
return this.request(
`/leave_requests/${encodeURIComponent(requestId)}`,
{
method: "PATCH",
body: updates,
}
);
}
// ── Groups ───────────────────────────────────────────
async listGroups(): Promise<unknown[]> {
return this.request("/groups");
}
async createGroup(data: {
name: string;
spokeId: string;
userIds: string[];
}): Promise<unknown> {
return this.request("/groups", {
method: "POST",
body: data,
});
}
async updateGroup(
groupId: string,
data: { name?: string; userIds?: string[] }
): Promise<unknown> {
return this.request(
`/groups/${encodeURIComponent(groupId)}`,
{
method: "PUT",
body: data,
}
);
}
async deleteGroup(groupId: string): Promise<void> {
await this.request(`/groups/${encodeURIComponent(groupId)}`, {
method: "DELETE",
});
}
// ── Leave Types ──────────────────────────────────────
async listLeaveTypes(): Promise<unknown[]> {
return this.request("/company_leave_types");
}
}