Freelo SDK
Official JavaScript/TypeScript SDK for Freelo.io API.
Features
- Auto-generated from OpenAPI — always in sync with the Freelo API
- Full TypeScript support with type definitions
- Zero runtime dependencies (uses native fetch)
- Tree-shakeable — import only what you need
- Works in Node.js 18+, browsers, and all major frameworks
- Promise-based async/await API
- Per-request auth for multi-tenant scenarios
- Built-in pagination, currency, and date utilities
Installation
npm install @freeloapp/js-sdk
Quick Start
import { createFreelo, getProjects, createTask } from '@freeloapp/js-sdk';
// Initialize client (sets global default)
createFreelo({
auth: { type: 'basic', email: '[email protected]', apiKey: 'your-api-key' },
userAgent: 'YourApp/1.0 ([email protected])',
});
// Get all projects
const { data: projects } = await getProjects();
// Create a task
const { data: task } = await createTask({
path: { tasklist_id: 123 },
body: {
name: 'New Task',
worker_ids: [userId],
},
});
Authentication
The SDK supports three authentication methods:
Basic Auth
Get your API key from Freelo Settings.
createFreelo({
auth: { type: 'basic', email: '[email protected]', apiKey: 'your-api-key' },
userAgent: 'YourApp/1.0',
});
Bearer Token
Use a pre-obtained JWT or PASETO token:
createFreelo({
auth: { type: 'bearer', token: 'your-jwt-token' },
userAgent: 'YourApp/1.0',
});
OAuth 2.1 (PKCE)
For applications that authenticate users via Freelo's identity provider (identity.freelo.io). The SDK handles automatic token refresh — you handle the initial authorization flow.
Step 1: Start the OAuth flow
import {
generatePKCEChallenge,
buildAuthorizationUrl,
} from '@freeloapp/js-sdk';
// Generate PKCE challenge (store verifier for step 2)
const pkce = await generatePKCEChallenge();
sessionStorage.setItem('pkce_verifier', pkce.codeVerifier);
// Build authorization URL and redirect the user
const authUrl = buildAuthorizationUrl({
clientId: 'your-client-id',
redirectUri: 'https://yourapp.com/callback',
codeChallenge: pkce.codeChallenge,
scope: ['read', 'write', 'projects', 'tasks'],
state: crypto.randomUUID(),
});
window.location.href = authUrl;
Step 2: Exchange the authorization code for tokens
import { exchangeCode } from '@freeloapp/js-sdk';
// In your callback handler (e.g. /callback)
const params = new URLSearchParams(window.location.search);
const tokens = await exchangeCode({
code: params.get('code')!,
codeVerifier: sessionStorage.getItem('pkce_verifier')!,
clientId: 'your-client-id',
redirectUri: 'https://yourapp.com/callback',
});
Step 3: Create the SDK client
import { createFreelo, getProjects } from '@freeloapp/js-sdk';
createFreelo({
auth: {
type: 'oauth',
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
clientId: 'your-client-id',
expiresAt: tokens.expiresAt,
onTokenRefreshed: (newTokens) => {
// Persist new tokens (localStorage, database, etc.)
localStorage.setItem('freelo_tokens', JSON.stringify(newTokens));
},
},
userAgent: 'YourApp/1.0',
});
// Use the SDK normally — token refresh happens automatically
const { data: projects } = await getProjects();
The SDK refreshes tokens proactively (30s before expiry) and reactively (on 401 responses). Concurrent requests share a single refresh call.
Additional OAuth utilities:
import {
refreshAccessToken, // Manually refresh a token
revokeToken, // Revoke an access or refresh token
discoverOAuthServer, // Fetch OAuth server metadata (RFC 8414)
} from '@freeloapp/js-sdk';
Configuration
import { createFreelo } from '@freeloapp/js-sdk';
const client = createFreelo({
auth: { // Required: authentication credentials
type: 'basic', // Basic Auth with email + API key
email: '[email protected]',
apiKey: 'your-api-key',
},
userAgent: 'YourApp/1.0', // Required: Identifies your application
baseUrl: 'https://api.freelo.io/v1', // Optional: API base URL
logging: true, // Optional: Log requests to console
headers: { // Optional: default headers on every request
'X-Custom-Header': 'custom-value',
},
});
Default headers
Use the headers option when you need a header sent on every request —
for example to add tracing or client-identification metadata, or when
User-Agent cannot be set from JavaScript (browsers, embedded web
views) and you need to identify the client via a custom header instead:
createFreelo({
auth: { type: 'bearer', token: '...' },
userAgent: 'YourApp/1.0',
headers: {
'X-Custom-Header': 'custom-value',
},
});
Per-request headers override these defaults. The built-in Authorization
and User-Agent headers always win — use auth and userAgent to change them.
Multi-Tenant / Per-Request Auth
Create multiple client instances for different users:
import { createFreelo, getProjects } from '@freeloapp/js-sdk';
// Basic Auth client
const basicClient = createFreelo({
auth: { type: 'basic', email: '[email protected]', apiKey: 'admin-key' },
userAgent: 'MyApp/1.0',
});
// Bearer token client
const tokenClient = createFreelo({
auth: { type: 'bearer', token: 'your-jwt-token' },
userAgent: 'MyApp/1.0',
});
// OAuth client
const oauthClient = createFreelo({
auth: { type: 'oauth', accessToken: '...', refreshToken: '...', clientId: '...' },
userAgent: 'MyApp/1.0',
});
// Use default client (last one created)
const { data: projects } = await getProjects();
// Or pass client explicitly
const { data: adminProjects } = await getProjects({ client: basicClient });
API Reference
All SDK functions are auto-generated from the Freelo OpenAPI spec. Each function maps directly to an API endpoint.
Projects
import {
getProjects,
getAllProjects,
getProject,
createProject,
deleteProject,
archiveProject,
activateProject,
getProjectWorkers,
} from '@freeloapp/js-sdk';
// List own active projects
const { data: projects } = await getProjects();
// List all projects (paginated)
const { data } = await getAllProjects({ query: { p: 0 } });
// Get project by ID
const { data: project } = await getProject({ path: { project_id: 42 } });
// Create project
const { data: newProject } = await createProject({
body: { name: 'New Project', currency: 'CZK' },
});
// Delete / archive / activate
await deleteProject({ path: { project_id: 42 } });
await archiveProject({ path: { project_id: 42 } });
await activateProject({ path: { project_id: 42 } });
Tasks
import {
getAllTasks,
getTask,
createTask,
editTask,
finishTask,
deleteTask,
moveTask,
} from '@freeloapp/js-sdk';
// List all tasks (paginated)
const { data } = await getAllTasks({ query: { p: 0 } });
// Get task by ID
const { data: task } = await getTask({ path: { task_id: 99 } });
// Create task in a tasklist
const { data: newTask } = await createTask({
path: { tasklist_id: 5 },
body: { name: 'New Task', worker_ids: [1] },
});
// Edit task (name, description, labels, worker, due dates, priority, tracking users)
await editTask({
path: { task_id: 99 },
body: {
name: 'Updated Name',
description: '<p>Rewritten task body</p>', // overwrites the whole description
},
});
// Finish / delete / move
await finishTask({ path: { task_id: 99 } });
await deleteTask({ path: { task_id: 99 } });
await moveTask({
path: { task_id: 99 },
body: { tasklist_id: 10 },
});
Task Labels
import {
findAvailableTaskLabels,
createTaskLabels,
addTaskLabelsToTask,
removeTaskLabelsFromTask,
mergeTaskLabels,
getTaskLabelColors,
} from '@freeloapp/js-sdk';
// All labels available to the caller, optionally narrowed to one project
const { data: all } = await findAvailableTaskLabels();
const { data: inProject } = await findAvailableTaskLabels({
query: { project_id: 42 },
});
// Fetch-or-create — an existing label with the same name AND color is reused
await createTaskLabels({ body: { labels: [{ name: 'bug', color: '#ff0000' }] } });
// Attach / detach on a task (by uuid, or by name + color)
await addTaskLabelsToTask({
path: { task_id: 99 },
body: { labels: [{ name: 'bug', color: '#ff0000' }] },
});
await removeTaskLabelsFromTask({
path: { task_id: 99 },
body: { labels: [{ uuid: 'aaaaaaaa-...' }] },
});
// Merge labels — every task carrying a source label ends up with the target label
await mergeTaskLabels({
body: {
from_uuids: ['aaaaaaaa-...', 'bbbbbbbb-...'],
to_uuid: 'cccccccc-...',
},
});
Merge notes: the target's name and color come from the existing to_uuid label,
all UUIDs must be owned by the caller (otherwise 404), the replacement applies
only to tasks in projects where the caller is a commander, and the source label
definitions are detached from tasks but not deleted.
Notifying yourself about your own actions
By default the API omits the action's author from the notification recipients.
Pass notify_author: true to keep yourself in the loop — useful for automations
running under your own token. Supported by createTask (and its subtasks),
editTask, activateTask, finishTask, editTaskcheck, finishTaskcheck,
createComment, editComment and editNote.
await createComment({
path: { task_id: 99 },
body: { content: 'Deployed to production', notify_author: true },
});
It only takes effect if you are otherwise a subscriber, worker or tracking user of the target.
Tasklists
import {
getAllTasklists,
getTasklist,
createTasklist,
} from '@freeloapp/js-sdk';
const { data } = await getAllTasklists();
const { data: tasklist } = await getTasklist({ path: { tasklist_id: 5 } });
const { data: newTasklist } = await createTasklist({
path: { project_id: 42 },
body: { name: 'New Tasklist' },
});
Comments
import { getAllComments, createComment, editComment } from '@freeloapp/js-sdk';
const { data } = await getAllComments({ path: { task_id: 99 } });
const { data: comment } = await createComment({
path: { task_id: 99 },
body: { content: 'Hello!' },
});
Time Tracking
import { startTimeTracking, stopTimeTracking, editTimeTracking } from '@freeloapp/js-sdk';
await startTimeTracking({
path: { task_id: 99 },
body: { note: 'Working on feature' },
});
await stopTimeTracking({ path: { task_id: 99 } });
Search
import { search } from '@freeloapp/js-sdk';
const { data: results } = await search({
body: { search: 'bug fix' },
});
Pagination
The SDK provides utilities for handling paginated responses:
import { fetchAllPages, iteratePages, createPaginator, getAllProjects } from '@freeloapp/js-sdk';
// Iterate through all items one by one
for await (const project of iteratePages(
(page) => getAllProjects({ query: { p: page } }),
(response) => response.data ?? []
)) {
console.log(project.name);
}
// Fetch all pages at once
const allProjects = await fetchAllPages(
(page) => getAllProjects({ query: { p: page } }),
(response) => response.data ?? []
);
// Create a reusable paginator
const paginator = createPaginator(
(page) => getAllProjects({ query: { p: page } }),
(response) => response.data ?? []
);
const all = await paginator.fetchAll();
Error Handling
import { getProject, isFreeloError, isRateLimited, isUnauthorized, isNotFound } from '@freeloapp/js-sdk';
const { data, error, response } = await getProject({ path: { project_id: 123 } });
if (error) {
if (isRateLimited(error)) {
console.log(`Rate limited, retry after ${response?.headers.get('Retry-After') ?? '?'} s`);
} else if (isUnauthorized(error)) {
console.log('Invalid credentials');
} else if (isNotFound(error)) {
console.log('Project not found');
} else if (isFreeloError(error)) {
console.log('API error:', error.message);
}
} else {
console.log('Project:', data);
}
Rate Limiting
The API rate-limits requests per Freelo user (token bucket). The limit is shared by all applications and tokens of the same user, and the exact numbers are configurable and may change — don't hardcode them.
- When exceeded, you'll receive a
429status code with aRetry-Afterheader (seconds). Wait at least that long before retrying; use exponential backoff on repeated 429s. The SDK logs a warning automatically. - Responses also carry informational IETF
RateLimitheaders, e.g.RateLimit-Policy: "default";q=60;w=20(quota ofqrequests perwseconds) andRateLimit: "default";r=42;t=5(rrequests remaining, full refill intseconds).Retry-Afteris authoritative.
Utility Functions
Date Utilities
import { dateToApi, dateFromApi, today, daysFromNow } from '@freeloapp/js-sdk';
dateToApi(new Date()); // "2024-01-15"
dateFromApi('2024-01-15'); // Date object
today(); // Today in API format
daysFromNow(7); // 7 days from now
Currency Utilities
import { currencyToApi, currencyFromApi, formatCurrency } from '@freeloapp/js-sdk';
currencyToApi(123.45); // "12345"
currencyFromApi("12345"); // 123.45
formatCurrency(123.45, 'CZK'); // "123,45 Kc"
Updating SDK When API Changes
The SDK is generated from the online OpenAPI spec at https://api.freelo.io/docs/v1/freelo-api.yaml.
npm run generate # Re-generate from latest spec
npm run generate:check # Verify generated code matches committed version
CI pipeline checks weekly for spec changes.
TypeScript Support
All types are auto-generated and fully typed:
import type { ProjectFull, TaskFull, UserBasic, TaskCreate } from '@freeloapp/js-sdk';
Migration from v1
v2 replaces the class-based API with tree-shakeable functions:
// v1 (class-based)
import { Freelo } from '@freeloapp/js-sdk';
const freelo = new Freelo({ email, apiKey, userAgent });
const projects = await freelo.projects.list();
// v2 (function-based, tree-shakeable)
import { createFreelo, getProjects } from '@freeloapp/js-sdk';
createFreelo({ auth: { type: 'basic', email, apiKey }, userAgent });
const { data: projects } = await getProjects();
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
License
MIT