Node.js SDK
A modern JavaScript/TypeScript client for the IdentityCall API with full async/await support.
Installation
npm install node-fetch form-data
# With TypeScript
npm install node-fetch form-data @types/node-fetchQuick Start
import { IdentityCallClient } from './identitycall.js';
const client = new IdentityCallClient();
// List recordings
const result = await client.recordings.list();
result.data.forEach(recording => {
console.log(`${recording.id}: ${recording.name}`);
});Complete Client Implementation
Create a file called identitycall.js (or identitycall.ts for TypeScript):
identitycall.js
import fetch from 'node-fetch';
import FormData from 'form-data';
import fs from 'fs';
// Error classes
export class IdentityCallError extends Error {
constructor(message, statusCode = null, response = null) {
super(message);
this.name = 'IdentityCallError';
this.statusCode = statusCode;
this.response = response;
}
}
export class AuthenticationError extends IdentityCallError {
constructor(message, ...args) {
super(message, ...args);
this.name = 'AuthenticationError';
}
}
export class PermissionError extends IdentityCallError {
constructor(message, ...args) {
super(message, ...args);
this.name = 'PermissionError';
}
}
export class NotFoundError extends IdentityCallError {
constructor(message, ...args) {
super(message, ...args);
this.name = 'NotFoundError';
}
}
export class ValidationError extends IdentityCallError {
constructor(message, ...args) {
super(message, ...args);
this.name = 'ValidationError';
}
}
export class RateLimitError extends IdentityCallError {
constructor(message, retryAfter = 60, ...args) {
super(message, ...args);
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
}
// Recordings resource
class RecordingsResource {
constructor(client) {
this._client = client;
}
async list(page = 1, perPage = 20) {
return this._client._request('GET', '/recordings', {
params: { page, per_page: perPage }
});
}
async create(file, options = {}) {
const formData = new FormData();
if (typeof file === 'string') {
formData.append('file', fs.createReadStream(file));
} else {
formData.append('file', file, options.filename || 'recording.mp3');
}
formData.append('language', options.language || 'en');
if (options.name) {
formData.append('name', options.name);
}
return this._client._request('POST', '/recordings', {
body: formData,
headers: formData.getHeaders()
});
}
async get(recordingId) {
return this._client._request('GET', `/recordings/${recordingId}`);
}
async update(recordingId, data) {
return this._client._request('PATCH', `/recordings/${recordingId}`, {
json: data
});
}
async delete(recordingId) {
return this._client._request('DELETE', `/recordings/${recordingId}`);
}
async transcription(recordingId) {
return this._client._request('GET', `/recordings/${recordingId}/transcription`);
}
async results(recordingId) {
return this._client._request('GET', `/recordings/${recordingId}/results`);
}
async summary(recordingId) {
return this._client._request('GET', `/recordings/${recordingId}/summary`);
}
async waitForCompletion(recordingId, timeout = 600000, pollInterval = 5000) {
const startTime = Date.now();
while (true) {
const result = await this.get(recordingId);
const recording = result.data;
if (recording.status === 'completed') {
return recording;
}
if (recording.status === 'failed') {
throw new IdentityCallError('Transcription failed');
}
if (Date.now() - startTime > timeout) {
throw new Error('Transcription timed out');
}
await new Promise(r => setTimeout(r, pollInterval));
}
}
}
// Main client
export class IdentityCallClient {
static DEFAULT_BASE_URL = 'https://api.identitycall.com/api/v1/public';
static MAX_RETRIES = 3;
constructor(options = {}) {
this.apiKey = options.apiKey || process.env.IDENTITYCALL_API_KEY;
if (!this.apiKey) {
throw new Error('API key is required. Set IDENTITYCALL_API_KEY or pass apiKey option.');
}
this.baseUrl = options.baseUrl || IdentityCallClient.DEFAULT_BASE_URL;
this.timeout = options.timeout || 30000;
// Resources
this.recordings = new RecordingsResource(this);
}
async _request(method, path, options = {}) {
const url = new URL(path, this.baseUrl);
if (options.params) {
Object.entries(options.params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
const headers = {
'Authorization': `Bearer ${this.apiKey}`,
'Accept': 'application/json',
...options.headers
};
let body = options.body;
if (options.json) {
headers['Content-Type'] = 'application/json';
body = JSON.stringify(options.json);
}
for (let attempt = 0; attempt < IdentityCallClient.MAX_RETRIES; attempt++) {
try {
const response = await fetch(url.toString(), {
method,
headers,
body,
timeout: this.timeout
});
return await this._handleResponse(response);
} catch (error) {
if (error instanceof RateLimitError && attempt < IdentityCallClient.MAX_RETRIES - 1) {
await new Promise(r => setTimeout(r, error.retryAfter * 1000));
continue;
}
if (error instanceof IdentityCallError &&
error.statusCode >= 500 &&
attempt < IdentityCallClient.MAX_RETRIES - 1) {
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
continue;
}
throw error;
}
}
}
async _handleResponse(response) {
if (response.status === 204) {
return null;
}
let data;
try {
data = await response.json();
} catch {
data = {};
}
if (response.ok) {
return data;
}
const errorMessage = data.error || 'Unknown error';
switch (response.status) {
case 401:
throw new AuthenticationError(errorMessage, response.status, data);
case 403:
throw new PermissionError(errorMessage, response.status, data);
case 404:
throw new NotFoundError(errorMessage, response.status, data);
case 422:
throw new ValidationError(errorMessage, response.status, data);
case 429:
throw new RateLimitError(errorMessage, data.retry_after || 60, response.status, data);
default:
throw new IdentityCallError(errorMessage, response.status, data);
}
}
}
export default IdentityCallClient;TypeScript Version
identitycall.ts
import fetch, { Response } from 'node-fetch';
import FormData from 'form-data';
import fs from 'fs';
// Types
interface Recording {
id: number;
name: string | null;
status: 'pending' | 'processing' | 'completed' | 'failed';
language: string;
duration_ms: number | null;
created_at: string;
updated_at: string;
}
interface Transcription {
full_text: string;
dialogues: Dialogue[];
}
interface Dialogue {
position: number;
start_ms: number;
end_ms: number;
speaker: string;
text: string;
emotion: Record<string, number>;
}
interface PaginatedResponse<T> {
data: T[];
meta: {
current_page: number;
per_page: number;
total_pages: number;
total_count: number;
};
}
interface SingleResponse<T> {
data: T;
}
interface ClientOptions {
apiKey?: string;
baseUrl?: string;
timeout?: number;
}
interface CreateOptions {
filename?: string;
language?: string;
name?: string;
}
// Client implementation with types
export class IdentityCallClient {
private apiKey: string;
private baseUrl: string;
private timeout: number;
public recordings: RecordingsResource;
constructor(options: ClientOptions = {}) {
this.apiKey = options.apiKey || process.env.IDENTITYCALL_API_KEY || '';
if (!this.apiKey) {
throw new Error('API key is required');
}
this.baseUrl = options.baseUrl || 'https://api.identitycall.com/api/v1/public';
this.timeout = options.timeout || 30000;
this.recordings = new RecordingsResource(this);
}
async _request<T>(method: string, path: string, options: any = {}): Promise<T> {
// Implementation same as JS version
// Returns typed response
}
}
class RecordingsResource {
constructor(private client: IdentityCallClient) {}
async list(page = 1, perPage = 20): Promise<PaginatedResponse<Recording>> {
return this.client._request('GET', '/recordings', {
params: { page, per_page: perPage }
});
}
async get(id: number): Promise<SingleResponse<Recording>> {
return this.client._request('GET', `/recordings/${id}`);
}
async transcription(id: number): Promise<SingleResponse<Transcription>> {
return this.client._request('GET', `/recordings/${id}/transcription`);
}
}Usage Examples
List Recordings
import { IdentityCallClient } from './identitycall.js';
const client = new IdentityCallClient();
// Get first page
const result = await client.recordings.list();
result.data.forEach(recording => {
console.log(`[${recording.id}] ${recording.name} - ${recording.status}`);
});
// With pagination
const page2 = await client.recordings.list(2, 50);
console.log(`Page ${page2.meta.current_page} of ${page2.meta.total_pages}`);Upload Recording
// From file path
const result = await client.recordings.create('./call.mp3', {
language: 'en',
name: 'Customer Support Call'
});
console.log(`Uploaded! ID: ${result.data.id}`);
// From stream
import fs from 'fs';
const stream = fs.createReadStream('./call.mp3');
const result2 = await client.recordings.create(stream, {
filename: 'call.mp3',
language: 'en'
});Wait for Transcription
// Upload and wait
const uploadResult = await client.recordings.create('./call.mp3');
const recordingId = uploadResult.data.id;
console.log('Waiting for transcription...');
const recording = await client.recordings.waitForCompletion(recordingId);
console.log(`Completed in ${recording.duration_ms}ms`);
// Get transcription
const transcription = await client.recordings.transcription(recordingId);
console.log(transcription.data.full_text);Get Analysis Results
const results = await client.recordings.results(recordingId);
// Goals
console.log('Goals:');
results.data.goals.forEach(goal => {
const status = goal.met ? '✓' : '✗';
console.log(` ${status} ${goal.goal_name}: ${goal.score.toFixed(2)}`);
});
// Keywords
console.log('\nKeywords:');
results.data.keywords.forEach(kw => {
console.log(` "${kw.keyword_name}" - ${kw.count} mentions`);
});Error Handling
import {
IdentityCallClient,
AuthenticationError,
PermissionError,
NotFoundError,
RateLimitError
} from './identitycall.js';
const client = new IdentityCallClient();
try {
const recording = await client.recordings.get(999999);
} catch (error) {
if (error instanceof NotFoundError) {
console.log('Recording not found');
} else if (error instanceof PermissionError) {
console.log('Permission denied');
} else if (error instanceof AuthenticationError) {
console.log('Invalid API key');
} else if (error instanceof RateLimitError) {
console.log(`Rate limited. Retry in ${error.retryAfter}s`);
} else {
throw error;
}
}Iterate All Pages
async function* iterateRecordings(perPage = 100) {
let page = 1;
while (true) {
const result = await client.recordings.list(page, perPage);
for (const recording of result.data) {
yield recording;
}
if (result.meta.current_page >= result.meta.total_pages) {
break;
}
page++;
}
}
// Usage
for await (const recording of iterateRecordings()) {
console.log(recording.name);
}Browser Usage
For browser environments, use the native fetch API:
// Browser-compatible client
class BrowserIdentityCallClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.identitycall.com/api/v1/public';
}
async request(method, path, options = {}) {
const url = new URL(path, this.baseUrl);
if (options.params) {
Object.entries(options.params).forEach(([k, v]) => {
url.searchParams.set(k, v);
});
}
const response = await fetch(url, {
method,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Accept': 'application/json',
...options.headers
},
body: options.body
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Request failed');
}
return response.status === 204 ? null : response.json();
}
async listRecordings(page = 1, perPage = 20) {
return this.request('GET', '/recordings', {
params: { page, per_page: perPage }
});
}
}