The official Node.js SDK for the Keito API — track billable time, expenses, and invoices for humans and AI agents.
Keito is the first purpose-built billable work tracker for AI agents, capturing not just what agents cost, but what they earn. The SDK supports time-based, outcome-based, and hybrid billing models.
npm install @keito-ai/sdkRequirements: Node.js 18+ (uses native
fetch). Zero runtime dependencies.
import { Keito } from '@keito-ai/sdk';
const client = new Keito({
apiKey: process.env.KEITO_API_KEY!,
accountId: process.env.KEITO_ACCOUNT_ID!,
});
// Log billable time for an AI agent
const entry = await client.timeEntries.create({
project_id: 'proj_123',
task_id: 'task_456',
spent_date: '2026-03-05',
hours: 1.5,
notes: 'Automated code review for PR #842',
source: 'agent',
billable: true,
metadata: {
agent_id: 'code-reviewer-v2',
run_id: 'run_abc123',
model: 'claude-4-sonnet',
tokens_used: 45200,
},
});
console.log(entry.id);The SDK uses API key authentication. Pass your key and account ID when creating the client:
const client = new Keito({
apiKey: 'kto_...', // Your Keito API key
accountId: 'acc_...', // Your Keito account ID
});We recommend using environment variables to keep your keys secure. Never commit API keys to source control.
| Option | Default | Description |
|---|---|---|
apiKey |
required | Your Keito API key (kto_...) |
accountId |
required | Your Keito account ID |
baseUrl |
https://app.keito.io |
API base URL |
maxRetries |
3 |
Max retries for failed requests |
timeout |
30000 |
Request timeout in milliseconds |
const client = new Keito({
apiKey: 'kto_...',
accountId: 'acc_...',
baseUrl: 'https://app.keito.io',
maxRetries: 3,
timeout: 30_000,
});The SDK follows a resource-based architecture. Each API resource is accessed as a property on the client:
| Resource | Methods | Description |
|---|---|---|
client.timeEntries |
list create update delete |
Log and manage billable time |
client.expenses |
list create |
Track expenses and compute costs |
client.projects |
list |
Browse projects |
client.clients |
list get create update |
Manage client records |
client.contacts |
list create |
Manage client contacts |
client.tasks |
list |
Browse tasks |
client.users |
me |
Get current user |
client.invoices |
list get create update delete |
Create and manage invoices |
client.invoices.messages |
list send |
Send invoices to clients |
client.reports |
teamTime agentSummary |
Reporting and analytics |
client.outcomes |
log |
Log outcome-based billable events |
// List time entries with filters
const entries = await client.timeEntries.list({
project_id: 'proj_123',
source: 'agent',
from: '2026-03-01',
to: '2026-03-31',
});
console.log(entries.data); // TimeEntry[]
// Create a time entry
const entry = await client.timeEntries.create({
project_id: 'proj_123',
task_id: 'task_456',
spent_date: '2026-03-05',
hours: 2.0,
source: 'agent',
billable: true,
});
// Update a time entry
await client.timeEntries.update('te_abc', {
hours: 2.5,
notes: 'Updated estimate',
});
// Delete a time entry
await client.timeEntries.delete('te_abc');const expense = await client.expenses.create({
project_id: 'proj_123',
expense_category_id: 'cat_compute',
spent_date: '2026-03-05',
total_cost: 4.20,
notes: 'GPU compute for batch inference',
source: 'agent',
billable: true,
metadata: {
provider: 'aws',
instance_type: 'p4d.24xlarge',
duration_minutes: 45,
},
});// Create an invoice
const invoice = await client.invoices.create({
client_id: 'cl_abc',
payment_term: 'net_30',
subject: 'March 2026 — Agent Services',
line_items: [
{
kind: 'Service',
description: 'AI code review — 42 PRs',
quantity: 42,
unit_price: 15.00,
taxed: false,
taxed2: false,
},
],
});
// Send the invoice
await client.invoices.messages.send(invoice.id!, {
recipients: [{ name: 'Jane Smith', email: 'jane@acme.com' }],
subject: 'Invoice from Keito',
body: 'Please find your invoice attached.',
attach_pdf: true,
send_me_a_copy: false,
include_attachments: false,
event_type: 'send',
});const me = await client.users.me();
console.log(me.email, me.user_type); // "agent@company.com" "agent"List methods return a PaginatedResponse<T> with built-in auto-pagination. You can iterate through all items across pages without managing page tokens:
// Auto-paginate through all time entries
for await (const entry of client.timeEntries.list({ source: 'agent' })) {
console.log(entry.id, entry.hours);
}You can also work with individual pages:
const page = await client.timeEntries.list({ per_page: 50 });
console.log(page.data); // TimeEntry[] — current page
console.log(page.total_entries); // total across all pages
console.log(page.total_pages);
if (page.hasNextPage()) {
const next = await page.nextPage();
}The SDK throws typed errors for different failure modes. All errors extend KeitoError:
import { Keito, KeitoApiError, KeitoRateLimitError } from '@keito-ai/sdk';
try {
await client.timeEntries.create({ ... });
} catch (err) {
if (err instanceof KeitoRateLimitError) {
console.log(`Rate limited. Retry after ${err.retryAfter}s`);
} else if (err instanceof KeitoApiError) {
console.log(err.status); // 400, 401, 403, 404, 409, etc.
console.log(err.error); // "bad_request"
console.log(err.error_description); // "project_id is required"
}
}| Error Class | Status | When |
|---|---|---|
KeitoAuthError |
401 | Invalid or missing API key |
KeitoForbiddenError |
403 | Insufficient permissions |
KeitoNotFoundError |
404 | Resource doesn't exist |
KeitoConflictError |
409 | Conflict (e.g. deleting approved entry) |
KeitoRateLimitError |
429 | Too many requests |
KeitoTimeoutError |
— | Request timed out |
KeitoConnectionError |
— | Network failure |
The SDK automatically retries on transient failures (status 408, 429, 500, 502, 503, 504) and network errors with exponential backoff and jitter. On 429 responses, the SDK respects the Retry-After header.
Retries are configurable:
const client = new Keito({
apiKey: 'kto_...',
accountId: 'acc_...',
maxRetries: 5, // default: 3
timeout: 60_000, // default: 30s
});Not all agent work is measured in hours. The SDK supports outcome-based billing for agents that deliver discrete results — resolved tickets, qualified leads, generated documents.
The outcomes.log() method is a convenience layer over time entries. It creates a TimeEntry with hours: 0 and outcome data packed into metadata:
import { Keito, OutcomeTypes } from '@keito-ai/sdk';
const client = new Keito({ apiKey: 'kto_...', accountId: 'acc_...' });
await client.outcomes.log({
project_id: 'proj_123',
task_id: 'task_456',
spent_date: '2026-03-05',
outcome: {
type: OutcomeTypes.TICKET_RESOLVED,
description: 'Resolved billing inquiry #4821 via email',
unit_price: 0.99,
quantity: 1,
success: true,
evidence: {
ticket_id: 'TKT-4821',
resolution_time_seconds: 45,
customer_confirmed: true,
},
},
metadata: {
agent_id: 'support-agent-v3',
run_id: 'run_xyz789',
},
});The SDK ships with pre-defined outcome type constants:
import { OutcomeTypes } from '@keito-ai/sdk';
OutcomeTypes.TICKET_RESOLVED // 'ticket_resolved'
OutcomeTypes.LEAD_QUALIFIED // 'lead_qualified'
OutcomeTypes.MEETING_BOOKED // 'meeting_booked'
OutcomeTypes.DOCUMENT_GENERATED // 'document_generated'
OutcomeTypes.CODE_REVIEW_COMPLETED // 'code_review_completed'
OutcomeTypes.DATA_PROCESSED // 'data_processed'
OutcomeTypes.EMAIL_SENT // 'email_sent'
OutcomeTypes.INVOICE_CREATED // 'invoice_created'
OutcomeTypes.WORKFLOW_COMPLETED // 'workflow_completed'
OutcomeTypes.CUSTOM // 'custom'You can also pass any string as a custom outcome type.
Get a combined time + outcome summary for an agent:
const summary = await client.reports.agentSummary({
user_id: 'agent_user_123',
from: '2026-03-01',
to: '2026-03-31',
});
console.log(summary.time); // { total_hours, billable_hours, revenue }
console.log(summary.outcomes); // { total, successful, revenue }
console.log(summary.combined); // { total_revenue }Time entries and expenses support a metadata field (arbitrary JSON, max 4KB) for storing agent-specific context. We recommend a consistent schema:
await client.timeEntries.create({
project_id: 'proj_123',
task_id: 'task_456',
spent_date: '2026-03-05',
hours: 1.5,
source: 'agent',
metadata: {
// Agent identity
agent_id: 'code-reviewer-v2',
framework: 'openclaw',
// Run context
run_id: 'run_abc123',
parent_run_id: 'run_parent456',
trigger: 'webhook',
// Model usage
model_provider: 'anthropic',
model_name: 'claude-4-sonnet',
tokens_in: 12500,
tokens_out: 32700,
cost_usd: 0.18,
// Quality
confidence: 0.94,
human_reviewed: false,
},
});Metadata uses merge semantics on update — new keys are merged into existing metadata. Set metadata to null to clear it.
The SDK is written in TypeScript and exports types for all API models:
import type {
TimeEntry,
TimeEntryCreate,
Expense,
Invoice,
Project,
Client,
User,
Metadata,
} from '@keito-ai/sdk';All method parameters and return types are fully typed. Your editor will provide autocomplete and type checking out of the box.
SDK types are auto-generated from the Keito OpenAPI v2 spec using openapi-typescript. When the spec changes:
npm run generate # regenerate src/generated/openapi.d.ts
npm run typecheck # verify all resource classes still compileThe CI pipeline includes a workflow that auto-opens a PR when the upstream spec changes.
Contributions are welcome. To get started:
git clone https://github.com/osodevops/keito-node.git
cd keito-node
npm install
npm run generate
npm test| Script | Description |
|---|---|
npm run generate |
Regenerate types from OpenAPI spec |
npm run typecheck |
Run TypeScript type checking |
npm test |
Run tests with Vitest |
npm run build |
Build ESM + CJS output |
MIT