API Reference

Time Entries

Create, update, list, and remove organization time entries.

REST /api/v1API key authentication5 endpoints
GET/api/v1/time-entries

List time entries with optional filters

Query schema
const dateSchema = z
  .string()
  .regex(/^\d{4}-\d{2}-\d{2}$/)
  .refine((value) => {
    const parsed = parseDateOnlyLocal(value);
    return parsed !== null && formatDateLocal(parsed) === value;
  }, 'Date must be a valid YYYY-MM-DD value');

const listQuerySchema = z.object({
  taskId: z.string().uuid().optional(),
  resourceId: z.string().uuid().optional(),
  projectId: z.string().uuid().optional(),
  dateFrom: dateSchema.optional(),
  dateTo: dateSchema.optional(),
  isBillable: z
    .string()
    .transform((v) => v === 'true')
    .optional(),
  isInvoiced: z
    .string()
    .transform((v) => v === 'true')
    .optional(),
  limit: z.coerce.number().int().min(1).max(100).default(50),
  offset: z.coerce.number().int().min(0).default(0),
});
Response200JSON response with data and pagination envelopes.
POST/api/v1/time-entries

Create a new time entry

JSON body schema
const dateSchema = z
  .string()
  .regex(/^\d{4}-\d{2}-\d{2}$/)
  .refine((value) => {
    const parsed = parseDateOnlyLocal(value);
    return parsed !== null && formatDateLocal(parsed) === value;
  }, 'Date must be a valid YYYY-MM-DD value');

const timeSchema = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, 'Time must be in HH:MM format');

const entrySourceSchema = z.enum(['manual', 'timer', 'copied', 'suggested']);

const createBodySchema = z
  .object({
    taskId: z.string().uuid(),
    resourceId: z.string().uuid(),
    date: dateSchema,
    hours: z.number().positive().max(24),
    durationSeconds: z
      .number()
      .int()
      .positive()
      .max(24 * 60 * 60)
      .optional(),
    startTime: timeSchema.optional(),
    entrySource: entrySourceSchema.optional(),
    description: z.string().nullable().optional(),
    isBillable: z.boolean().default(true),
    billableRate: z.number().optional(),
    tags: z.array(z.string()).optional(),
  })
  .superRefine((value, ctx) => {
    if ((value.entrySource ?? 'manual') !== 'timer' && value.hours < 0.25) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Hours must be at least 0.25 for non-timer entries',
        path: ['hours'],
      });
    }
  });
Response201JSON response.
DELETE/api/v1/time-entries/:id

Delete a time entry

Path parametersid: string
Response204No response body.
GET/api/v1/time-entries/:id

Get a single time entry

Path parametersid: string
Response200JSON response with a data envelope.
PATCH/api/v1/time-entries/:id

Update a time entry

Path parametersid: string
JSON body schema
const dateSchema = z
  .string()
  .regex(/^\d{4}-\d{2}-\d{2}$/)
  .refine((value) => {
    const parsed = parseDateOnlyLocal(value);
    return parsed !== null && formatDateLocal(parsed) === value;
  }, 'Date must be a valid YYYY-MM-DD value');

const timeSchema = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, 'Time must be in HH:MM format');

const updateBodySchema = z.object({
  taskId: z.string().uuid().optional(),
  resourceId: z.string().uuid().optional(),
  date: dateSchema.optional(),
  hours: z.number().min(0.25).max(24).optional(),
  durationSeconds: z
    .number()
    .int()
    .positive()
    .max(24 * 60 * 60)
    .optional(),
  startTime: timeSchema.nullable().optional(),
  description: z.string().nullable().optional(),
  isBillable: z.boolean().optional(),
  billableRate: z.number().nullable().optional(),
  tags: z.array(z.string()).optional(),
});
Response200JSON response.