Documentation
¶
Index ¶
- Constants
- Variables
- func CentsFromReportNumber(v float64) int64
- func ListAll[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string) ([]T, error)
- func ListAllFuncWithOptions[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string, ...) error
- func ListAllWithOptions[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string, ...) ([]T, error)
- type APIError
- type CircuitBreaker
- type CircuitBreakerConfig
- type CircuitBreakerOpenError
- type Client
- func (c *Client) AuditLogBaseURL() string
- func (c *Client) Close()
- func (c *Client) Delete(ctx context.Context, path string) error
- func (c *Client) DeleteReports(ctx context.Context, path string) error
- func (c *Client) DeleteWithBody(ctx context.Context, path string, body any, out any) error
- func (c *Client) DeleteWithQuery(ctx context.Context, path string, query map[string]string) error
- func (c *Client) DeleteWithQueryCapture(ctx context.Context, path string, query map[string]string, out any) error
- func (c *Client) DocumentedBaseURL(host string) string
- func (c *Client) Get(ctx context.Context, path string, query map[string]string, out any) error
- func (c *Client) GetReports(ctx context.Context, path string, query map[string]string, out any) error
- func (c *Client) GetValues(ctx context.Context, path string, query url.Values, out any) error
- func (c *Client) Patch(ctx context.Context, path string, body any, out any) error
- func (c *Client) Post(ctx context.Context, path string, body any, out any) error
- func (c *Client) PostAuditLog(ctx context.Context, path string, body any, out any) error
- func (c *Client) PostMultipart(ctx context.Context, path string, form url.Values, out any) error
- func (c *Client) PostMultipartWithFiles(ctx context.Context, path string, form url.Values, files []MultipartFile, ...) error
- func (c *Client) PostReports(ctx context.Context, path string, body any, out any) error
- func (c *Client) PostReportsRaw(ctx context.Context, path string, body any) (*RawResponse, error)
- func (c *Client) PostWithQuery(ctx context.Context, path string, query map[string]string, body any, out any) error
- func (c *Client) Put(ctx context.Context, path string, body any, out any) error
- func (c *Client) PutMultipart(ctx context.Context, path string, form url.Values, out any) error
- func (c *Client) PutReports(ctx context.Context, path string, body any, out any) error
- func (c *Client) PutWithQuery(ctx context.Context, path string, query map[string]string, body any, out any) error
- func (c *Client) ReportsBaseURL() string
- func (c *Client) RequestRawValues(ctx context.Context, reportsHost bool, method, path string, query url.Values, ...) (*RawResponse, error)
- func (c *Client) RequestRawValuesForHost(ctx context.Context, host, method, path string, query url.Values, body any) (*RawResponse, error)
- func (c *Client) SetCircuitBreaker(cfg CircuitBreakerConfig)
- func (c *Client) SetUserAgent(ua string)
- type ClientEntity
- type ErrorTranslation
- type ListAllOptions
- type MultipartFile
- type PaginationCapError
- type Project
- type ProjectMembership
- type Rate
- type RawResponse
- type Tag
- type Task
- type TimeEntry
- type TimeInterval
- type User
- type Workspace
Constants ¶
const ( DocumentedHostMain = "https://api.clockify.me/api/v1" DocumentedHostReports = "https://reports.api.clockify.me/v1" DocumentedHostAuditLog = "https://auditlog-api.api.clockify.me/v1" )
Variables ¶
var ErrCircuitBreakerOpen = errors.New("clockify upstream circuit breaker open")
Functions ¶
func CentsFromReportNumber ¶
CentsFromReportNumber converts a JSON number coming from the reports host (always integer-cents-as-float, per findings/rates-and-reports.md) to int64 cents using banker's rounding. The raw float is preserved by the caller for audit.
func ListAll ¶
func ListAll[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string) ([]T, error)
ListAll fetches every page into a single slice, bounded by the default pagination row cap.
func ListAllFuncWithOptions ¶
func ListAllFuncWithOptions[T any](ctx context.Context, c *Client, path string, baseQuery map[string]string, opts ListAllOptions, onPage func([]T) error) error
ListAllFuncWithOptions scans pages using opts and invokes onPage for each non-empty page without retaining earlier pages.
Types ¶
type APIError ¶
type APIError struct {
Method string
Path string
StatusCode int
Status string
Body string
RetryAfter time.Duration
Translation *ErrorTranslation
}
func (*APIError) ClientFacingMessage ¶
ClientFacingMessage returns a clean, human-readable message for the MCP client: the upstream message text without the HTTP method/path prefix or the internal clockify_error_code suffix. Server-side logs still call Error() for the full diagnostic.
func (*APIError) ErrorTranslation ¶
func (*APIError) Sanitized ¶
Sanitized returns the error string without the upstream response body, suitable for hosted/shared deployments where the body might leak per-tenant information across tenants. Server-side logs still call Error() so operators can see the full diagnostic; the sanitised form is only what we hand to the MCP client.
type CircuitBreaker ¶
type CircuitBreaker struct {
// contains filtered or unexported fields
}
func NewCircuitBreaker ¶
func NewCircuitBreaker(cfg CircuitBreakerConfig) *CircuitBreaker
func (*CircuitBreaker) After ¶
func (b *CircuitBreaker) After(endpoint, method string, upstreamFailure bool)
func (*CircuitBreaker) Before ¶
func (b *CircuitBreaker) Before(endpoint, method string) error
type CircuitBreakerConfig ¶
type CircuitBreakerOpenError ¶
func (*CircuitBreakerOpenError) Error ¶
func (e *CircuitBreakerOpenError) Error() string
func (*CircuitBreakerOpenError) Is ¶
func (e *CircuitBreakerOpenError) Is(target error) bool
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
func (*Client) AuditLogBaseURL ¶
AuditLogBaseURL returns the dedicated Clockify audit-log API base URL. Like ReportsBaseURL, custom/test base URLs remain unchanged.
func (*Client) DeleteReports ¶
DeleteReports performs a DELETE against the reports host.
func (*Client) DeleteWithBody ¶
func (*Client) DeleteWithQuery ¶
func (*Client) DeleteWithQueryCapture ¶ added in v0.2.0
func (c *Client) DeleteWithQueryCapture(ctx context.Context, path string, query map[string]string, out any) error
DeleteWithQueryCapture issues a DELETE with optional query parameters and decodes any response body into out. Clockify DELETE routes are inconsistent: most reply with an empty body, but some return the deleted entity. doJSON returns nil for a zero-length body, so out is left at its zero value when the server sends nothing. out must be non-nil.
func (*Client) DocumentedBaseURL ¶
DocumentedBaseURL resolves a generated OpenAPI host to the concrete base URL this client should use.
func (*Client) GetReports ¶
func (c *Client) GetReports(ctx context.Context, path string, query map[string]string, out any) error
GetReports performs a GET against the reports host. Use this for shared-report endpoints; everything else should stay on Get.
func (*Client) PostAuditLog ¶
PostAuditLog performs a POST against the audit-log host. The Clockify audit-log API lives on a separate host (auditlog-api.api.clockify.me); every other call stays on Post against the primary host.
func (*Client) PostMultipart ¶
PostMultipart performs a POST with a multipart/form-data body. Each form key maps to one or more values; multi-value keys are written as repeated form fields under the same name.
func (*Client) PostMultipartWithFiles ¶
func (c *Client) PostMultipartWithFiles(ctx context.Context, path string, form url.Values, files []MultipartFile, out any) error
PostMultipartWithFiles performs a POST with form fields plus binary file parts. File payloads are bounded to keep MCP requests from becoming an unbounded memory sink.
func (*Client) PostReports ¶
PostReports performs a POST against the reports host.
func (*Client) PostReportsRaw ¶
PostReportsRaw is the binary-aware sibling of PostReports. It is used for report exportType values such as PDF/CSV/XLSX/ZIP where the reports API may return a file body rather than JSON.
func (*Client) PostWithQuery ¶
func (*Client) PutMultipart ¶
PutMultipart performs a PUT with a multipart/form-data body. Same encoding rules as PostMultipart.
func (*Client) PutReports ¶
PutReports performs a PUT against the reports host.
func (*Client) PutWithQuery ¶
func (*Client) ReportsBaseURL ¶
ReportsBaseURL returns the base URL for endpoints that live on Clockify's reports host (reports.api.clockify.me/v1). The reports API is a separate host from api.clockify.me — hitting the reports paths on the primary host returns 404 ("No static resource"), and hitting them on the reports host with the /api/v1 prefix is also 404. See findings/shared-reports.md.
Derivation: when the primary base URL matches the canonical production host, swap to reports.api.clockify.me and drop the /api segment. Otherwise (test stubs, custom proxies, on-prem mirrors), return the primary base URL unchanged so existing fixtures continue to work without per-test wiring.
func (*Client) RequestRawValues ¶
func (c *Client) RequestRawValues(ctx context.Context, reportsHost bool, method, path string, query url.Values, body any) (*RawResponse, error)
RequestRawValues runs a request against a documented host and returns the raw response bytes and headers instead of unmarshalling JSON.
func (*Client) RequestRawValuesForHost ¶
func (c *Client) RequestRawValuesForHost(ctx context.Context, host, method, path string, query url.Values, body any) (*RawResponse, error)
RequestRawValuesForHost is the host-aware binary-aware request transport. Non-canonical test/custom base URLs stay pinned to the configured base so fake servers and proxies do not need per-host wiring.
func (*Client) SetCircuitBreaker ¶
func (c *Client) SetCircuitBreaker(cfg CircuitBreakerConfig)
func (*Client) SetUserAgent ¶
SetUserAgent sets the User-Agent string sent with every request.
type ClientEntity ¶
type ClientEntity struct {
ID string `json:"id"`
Name string `json:"name"`
Address string `json:"address,omitempty"`
Archived bool `json:"archived,omitempty"`
CCEmails any `json:"ccEmails,omitempty"`
CurrencyCode string `json:"currencyCode,omitempty"`
CurrencyID string `json:"currencyId,omitempty"`
Email string `json:"email,omitempty"`
Note string `json:"note,omitempty"`
WorkspaceID string `json:"workspaceId,omitempty"`
}
type ErrorTranslation ¶
type ErrorTranslation struct {
Message string `json:"message"`
Remediation string `json:"remediation,omitempty"`
Refs []string `json:"refs,omitempty"`
}
func TranslateAPIError ¶
func TranslateAPIError(statusCode int, body string) ErrorTranslation
type ListAllOptions ¶
type ListAllOptions struct {
// MaxRows bounds total decoded rows before ListAll/ListAllFunc fail
// closed. 0 uses the default production cap.
MaxRows int
}
type MultipartFile ¶
MultipartFile is a single file part for multipart/form-data requests. Data is already decoded by the caller; callers must not pass filesystem paths because MCP clients should not grant arbitrary server file access.
type PaginationCapError ¶
PaginationCapError reports that a paginated scan exceeded its row cap.
func (*PaginationCapError) Error ¶
func (e *PaginationCapError) Error() string
type Project ¶
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
ClientID string `json:"clientId,omitempty"`
ClientName string `json:"clientName,omitempty"`
Client any `json:"client,omitempty"`
Color string `json:"color,omitempty"`
Archived bool `json:"archived"`
Billable bool `json:"billable,omitempty"`
BudgetEstimate any `json:"budgetEstimate,omitempty"`
CostRate *Rate `json:"costRate,omitempty"`
Currency any `json:"currency,omitempty"`
CustomFields any `json:"customFields,omitempty"`
Duration string `json:"duration,omitempty"`
Estimate any `json:"estimate,omitempty"`
EstimateReset any `json:"estimateReset,omitempty"`
Expenses any `json:"expenses,omitempty"`
Favorite bool `json:"favorite,omitempty"`
HourlyRate *Rate `json:"hourlyRate,omitempty"`
Memberships []ProjectMembership `json:"memberships,omitempty"`
Note string `json:"note,omitempty"`
Public bool `json:"public,omitempty"`
Tasks []Task `json:"tasks,omitempty"`
Template bool `json:"template,omitempty"`
TimeEstimate any `json:"timeEstimate,omitempty"`
WorkspaceID string `json:"workspaceId,omitempty"`
}
type ProjectMembership ¶
type ProjectMembership struct {
UserID string `json:"userId"`
TargetID string `json:"targetId,omitempty"`
MembershipType string `json:"membershipType,omitempty"`
MembershipStatus string `json:"membershipStatus,omitempty"`
HourlyRate *Rate `json:"hourlyRate,omitempty"`
CostRate *Rate `json:"costRate,omitempty"`
}
ProjectMembership models the `memberships[]` element returned on projects, tasks, workspaces, and users. Per-member rates here override their parent scope; nil means "fall through to parent."
type Rate ¶
type Rate struct {
Amount int64 `json:"amount"`
Currency string `json:"currency,omitempty"`
Inherited bool `json:"inherited,omitempty"`
}
Rate is Clockify's RateDtoV1 / HourlyRateDtoV1. The Amount is always in the minor unit of the currency (cents for EUR/USD/GBP, etc.), as an integer. e.g. {amount: 4321, currency: "EUR"} represents €43.21.
Clockify can also serialise a rate as the literal string "##default", meaning "inherit from the parent scope (workspace/project/task)." We preserve that as Inherited=true with Amount=0 so callers can render it as "(inherited)" rather than dropping it.
func (*Rate) DecimalString ¶
DecimalString renders Amount/100 with exactly two decimal places. Negative amounts are preserved verbatim. e.g. 4321 -> "43.21", 100 -> "1.00", -250 -> "-2.50".
func (*Rate) Display ¶
Display returns the rate in "<symbol><decimal> <CCY>" form for the few currencies we know symbols for, falling back to "<decimal> <CCY>". Examples: 4321 EUR -> "€43.21 EUR"; 4321 USD -> "$43.21 USD"; 4321 XYZ -> "43.21 XYZ".
func (*Rate) MarshalJSON ¶
func (*Rate) UnmarshalJSON ¶
type RawResponse ¶
RawResponse is the binary-aware envelope returned by RequestRawValues and PostReportsRaw — the raw response bytes plus enough header context for callers to recover Content-Type and Content-Disposition (filename). Used by export endpoints where the upstream returns binary (PDF/XLSX) or non-JSON text (CSV).
type Task ¶
type Task struct {
ID string `json:"id"`
Name string `json:"name"`
ProjectID string `json:"projectId"`
AssigneeID string `json:"assigneeId,omitempty"`
AssigneeIDs []string `json:"assigneeIds,omitempty"`
Billable bool `json:"billable"`
BudgetEstimate int64 `json:"budgetEstimate,omitempty"`
CostRate *Rate `json:"costRate,omitempty"`
Duration string `json:"duration,omitempty"`
Estimate string `json:"estimate,omitempty"`
HourlyRate *Rate `json:"hourlyRate,omitempty"`
Status string `json:"status,omitempty"`
UserGroupIDs []string `json:"userGroupIds,omitempty"`
}
type TimeEntry ¶
type TimeEntry struct {
ID string `json:"id"`
Description string `json:"description"`
ProjectID string `json:"projectId"`
ProjectName string `json:"projectName,omitempty"`
TaskID string `json:"taskId,omitempty"`
TagIDs []string `json:"tagIds,omitempty"`
Billable bool `json:"billable"`
BillablePresent bool `json:"-"`
CostRate *Rate `json:"costRate,omitempty"`
CustomFieldValues any `json:"customFieldValues,omitempty"`
HourlyRate *Rate `json:"hourlyRate,omitempty"`
IsLocked bool `json:"isLocked,omitempty"`
KioskID string `json:"kioskId,omitempty"`
Type string `json:"type,omitempty"`
UserID string `json:"userId,omitempty"`
WorkspaceID string `json:"workspaceId,omitempty"`
TimeInterval TimeInterval `json:"timeInterval"`
}
func (TimeEntry) DurationSeconds ¶
func (*TimeEntry) UnmarshalJSON ¶
type TimeInterval ¶
type User ¶
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
ActiveWorkspace string `json:"activeWorkspace,omitempty"`
CustomFields any `json:"customFields,omitempty"`
DefaultWorkspace string `json:"defaultWorkspace,omitempty"`
Memberships any `json:"memberships,omitempty"`
ProfilePicture string `json:"profilePicture,omitempty"`
Settings any `json:"settings,omitempty"`
Status string `json:"status,omitempty"`
}
type Workspace ¶
type Workspace struct {
ID string `json:"id"`
Name string `json:"name"`
CakeOrganizationID string `json:"cakeOrganizationId,omitempty"`
CostRate *Rate `json:"costRate,omitempty"`
Currencies any `json:"currencies,omitempty"`
FeatureSubscriptionType string `json:"featureSubscriptionType,omitempty"`
Features []string `json:"features,omitempty"`
HourlyRate *Rate `json:"hourlyRate,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
Memberships []ProjectMembership `json:"memberships,omitempty"`
Subdomain any `json:"subdomain,omitempty"`
WorkspaceSettings any `json:"workspaceSettings,omitempty"`
}