// Drop-in Go client library for the KI BMS HTTP API. // // Save this file alongside your code as `ats_client.go` (or in its // own subpackage) and use the Client type: // // import "yourproject/ats_client" // // c := ats_client.New("pat_...") // rows, err := c.AccountList(&ats_client.ListOpts{Limit: 20, Sort: "-created_at"}) // // Every endpoint exposed by the HTTP API is wrapped as a // `` method on Client. List endpoints take *ListOpts; // get/update/delete endpoints take the row id as their first argument. // // Provided as-is, with no warranty. Vendor freely; modify as needed. // Targets Go 1.21+; uses only the standard library. // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. // Local edits will be overwritten by the once-per-day version check. package ats_client import ( "bytes" "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" ) // ── Identity (substituted at generation time) ─────────────────────── const ( AppSlug = "ats" AppName = "KI BMS" ModuleName = "ats_client" ClientVersion = "0.3.13" Language = "go" defaultBase = "https://ki-bewerber-management.de" ) // TypesJSON is the per-type metadata baked at generation time. // Available at runtime when calling code needs to know the legal // filters / sort columns / max_limit for a model without a second // round-trip. const TypesJSON = `{"application":{"ops":["list","read","create","update","delete"],"create_fields":["job_id","candidate_id","stage","previous_stage","position","applied_at","last_stage_at","source_id","source_label","cover_letter","cv_blob_id","cv_url","answers","fit_score","fit_reasoning","fit_flags","fit_computed_at","rejected_reason","rejected_note","tags"],"update_fields":["stage","previous_stage","position","applied_at","last_stage_at","source_id","source_label","cover_letter","cv_blob_id","cv_url","answers","fit_score","fit_reasoning","fit_flags","fit_computed_at","rejected_reason","rejected_note","tags"],"allowed_filters":["data__job_id","data__candidate_id","data__stage","data__source_id","data__rejected_reason","data__is_archived","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__applied_at","data__fit_score","data__position","data__last_stage_at"],"default_sort":"data__position","max_limit":500,"fields":[{"name":"tags","type":"tags"},{"name":"stage","type":"enum","values":["new","review","screening","interview","offer","hired","rejected","talent_pool"]},{"name":"cv_url","type":"url","max_len":2048},{"name":"job_id","type":"string","max_len":64,"ref":{"type":"job","owned":false,"optional":false}},{"name":"answers","type":"list"},{"name":"position","type":"number"},{"name":"fit_flags","type":"tags"},{"name":"fit_score","type":"number"},{"name":"source_id","type":"string","max_len":64,"ref":{"type":"source","owned":false,"optional":false}},{"name":"applied_at","type":"string","max_len":32},{"name":"cv_blob_id","type":"string","max_len":64},{"name":"candidate_id","type":"string","max_len":64,"ref":{"type":"candidate","owned":false,"optional":false}},{"name":"cover_letter","type":"string","max_len":16000},{"name":"source_label","type":"string","max_len":200},{"name":"fit_reasoning","type":"string","max_len":4000},{"name":"last_stage_at","type":"string","max_len":32},{"name":"rejected_note","type":"string","max_len":2000},{"name":"previous_stage","type":"string","max_len":32},{"name":"fit_computed_at","type":"string","max_len":32},{"name":"rejected_reason","type":"enum","values":["not_qualified","salary_mismatch","location_mismatch","culture_mismatch","withdrew","ghosted","filled_internally","duplicate","other"]}]},"application_note":{"ops":["list","read","create","update","delete"],"create_fields":["body","pinned","private","parent_kind","parent_id"],"update_fields":["body","pinned","private"],"allowed_filters":["data__parent_id","data__parent_kind","data__pinned","data__private","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":8000},{"name":"pinned","type":"bool"},{"name":"private","type":"bool"},{"name":"parent_id","type":"string","max_len":64},{"name":"parent_kind","type":"enum","values":["candidate","application","job"]}]},"candidate":{"ops":["list","read","create","update","delete"],"create_fields":["name","first_name","last_name","salutation","pronouns","email","phone","city","country","current_company","current_role","years_experience","available_from","salary_expectation","currency","linkedin","github","portfolio","cv_url","cv_blob_id","avatar_blob_id","summary","skills","languages","tags","source_id","source_label","pool_status","gdpr_consent","gdpr_consent_at","gdpr_retention_until","preferred_locale","last_touched_at","color"],"update_fields":["name","first_name","last_name","salutation","pronouns","email","phone","city","country","current_company","current_role","years_experience","available_from","salary_expectation","currency","linkedin","github","portfolio","cv_url","cv_blob_id","avatar_blob_id","summary","skills","languages","tags","source_id","source_label","pool_status","gdpr_consent","gdpr_consent_at","gdpr_retention_until","preferred_locale","last_touched_at","color"],"allowed_filters":["data__email","data__name","data__location","data__country","data__source_id","data__tags","data__skills","data__pool_status","data__gdpr_consent","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__name","data__last_touched_at"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"city","type":"string","max_len":120},{"name":"name","type":"string","max_len":200},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"email","type":"string","max_len":320},{"name":"phone","type":"string","max_len":64},{"name":"cv_url","type":"url","max_len":2048},{"name":"github","type":"url","max_len":2048},{"name":"skills","type":"tags"},{"name":"country","type":"string","max_len":120},{"name":"summary","type":"string","max_len":4000},{"name":"currency","type":"string","max_len":8},{"name":"linkedin","type":"url","max_len":2048},{"name":"pronouns","type":"string","max_len":32},{"name":"languages","type":"tags"},{"name":"last_name","type":"string","max_len":120},{"name":"portfolio","type":"url","max_len":2048},{"name":"source_id","type":"string","max_len":64,"ref":{"type":"source","owned":false,"optional":false}},{"name":"cv_blob_id","type":"string","max_len":64},{"name":"first_name","type":"string","max_len":120},{"name":"salutation","type":"enum","values":["herr","frau","divers","neutral"]},{"name":"pool_status","type":"enum","values":["active","talent_pool","blocked","withdrawn"]},{"name":"current_role","type":"string","max_len":200},{"name":"gdpr_consent","type":"bool"},{"name":"source_label","type":"string","max_len":200},{"name":"available_from","type":"string","max_len":32},{"name":"avatar_blob_id","type":"string","max_len":64},{"name":"current_company","type":"string","max_len":200},{"name":"gdpr_consent_at","type":"string","max_len":32},{"name":"last_touched_at","type":"string","max_len":32},{"name":"preferred_locale","type":"string","max_len":16},{"name":"years_experience","type":"number"},{"name":"salary_expectation","type":"number"},{"name":"gdpr_retention_until","type":"string","max_len":32}]},"email_template":{"ops":["list","read","create","update","delete"],"create_fields":["name","category","subject","body","language","stage_trigger","auto_send","active","variables_doc"],"update_fields":["name","category","subject","body","language","stage_trigger","auto_send","active","variables_doc"],"allowed_filters":["data__name","data__category","data__stage_trigger","data__active","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__name"],"default_sort":"data__name","max_limit":100,"fields":[{"name":"body","type":"string","max_len":16000},{"name":"name","type":"string","max_len":200},{"name":"active","type":"bool"},{"name":"subject","type":"string","max_len":400},{"name":"category","type":"enum","values":["acknowledge","screening_invite","interview_invite","rejection","offer","talent_pool","other"]},{"name":"language","type":"string","max_len":16},{"name":"auto_send","type":"bool"},{"name":"stage_trigger","type":"enum","values":["","new","review","screening","interview","offer","hired","rejected","talent_pool"]},{"name":"variables_doc","type":"string","max_len":2000}]},"evaluation":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","interview_id","interviewer_id","skills_score","culture_score","communication_score","potential_score","overall_score","recommendation","highlights","concerns","summary"],"update_fields":["skills_score","culture_score","communication_score","potential_score","overall_score","recommendation","highlights","concerns","summary"],"allowed_filters":["data__application_id","data__interview_id","data__interviewer_id","data__recommendation","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__overall_score"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"summary","type":"string","max_len":4000},{"name":"concerns","type":"string","max_len":4000},{"name":"highlights","type":"string","max_len":4000},{"name":"interview_id","type":"string","max_len":64,"ref":{"type":"interview","owned":false,"optional":false}},{"name":"skills_score","type":"number"},{"name":"culture_score","type":"number"},{"name":"overall_score","type":"number"},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"interviewer_id","type":"string","max_len":64},{"name":"recommendation","type":"enum","values":["strong_yes","yes","neutral","no","strong_no"]},{"name":"potential_score","type":"number"},{"name":"communication_score","type":"number"}]},"interview":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","candidate_id","job_id","kind","status","title","scheduled_at","duration_minutes","location","meeting_url","interviewer_id","interviewer_ids","agenda","notes","send_invite"],"update_fields":["kind","status","title","scheduled_at","duration_minutes","location","meeting_url","interviewer_id","interviewer_ids","agenda","notes","send_invite"],"allowed_filters":["data__application_id","data__candidate_id","data__job_id","data__kind","data__status","data__interviewer_id","status","is_archived","owned_by","created_by"],"allowed_sorts":["data__scheduled_at","created_at","updated_at"],"default_sort":"data__scheduled_at","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["phone","video","onsite","take_home","panel","trial_day"]},{"name":"notes","type":"string","max_len":8000},{"name":"title","type":"string","max_len":200},{"name":"agenda","type":"string","max_len":4000},{"name":"job_id","type":"string","max_len":64},{"name":"status","type":"enum","values":["scheduled","completed","no_show","cancelled","rescheduled"]},{"name":"location","type":"string","max_len":200},{"name":"meeting_url","type":"url","max_len":2048},{"name":"send_invite","type":"bool"},{"name":"candidate_id","type":"string","max_len":64},{"name":"scheduled_at","type":"string","max_len":32},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"interviewer_id","type":"string","max_len":64},{"name":"interviewer_ids","type":"list"},{"name":"duration_minutes","type":"number"}]},"job":{"ops":["list","read","create","update","delete"],"create_fields":["title","slug","department","location","country","remote","employment_type","seniority","headcount","salary_min","salary_max","currency","salary_visibility","summary","description","responsibilities","requirements","nice_to_have","benefits","language","status","public","ai_screen_enabled","ai_screen_prompt","knockout_questions","screening_questions","tags","hiring_manager_id","team_ids","opened_at","target_close_date","closed_at","external_apply_url","color"],"update_fields":["title","slug","department","location","country","remote","employment_type","seniority","headcount","salary_min","salary_max","currency","salary_visibility","summary","description","responsibilities","requirements","nice_to_have","benefits","language","status","public","ai_screen_enabled","ai_screen_prompt","knockout_questions","screening_questions","tags","hiring_manager_id","team_ids","opened_at","target_close_date","closed_at","external_apply_url","color"],"allowed_filters":["data__title","data__department","data__location","data__employment_type","data__seniority","data__remote","data__status","data__public","data__hiring_manager_id","data__tags","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__title","data__opened_at","data__target_close_date"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"slug","type":"string","max_len":120},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"title","type":"string","max_len":200},{"name":"public","type":"bool"},{"name":"remote","type":"enum","values":["onsite","hybrid","remote"]},{"name":"status","type":"enum","values":["draft","open","paused","closed","filled"]},{"name":"country","type":"string","max_len":80},{"name":"summary","type":"string","max_len":600},{"name":"benefits","type":"string","max_len":4000},{"name":"currency","type":"string","max_len":8},{"name":"language","type":"string","max_len":16},{"name":"location","type":"string","max_len":120},{"name":"team_ids","type":"list"},{"name":"closed_at","type":"string","max_len":32},{"name":"headcount","type":"number"},{"name":"opened_at","type":"string","max_len":32},{"name":"seniority","type":"enum","values":["junior","mid","senior","lead","principal"]},{"name":"department","type":"string","max_len":120},{"name":"salary_max","type":"number"},{"name":"salary_min","type":"number"},{"name":"description","type":"string","max_len":16000},{"name":"nice_to_have","type":"string","max_len":4000},{"name":"requirements","type":"string","max_len":8000},{"name":"employment_type","type":"enum","values":["full_time","part_time","internship","working_student","freelance","contract"]},{"name":"ai_screen_prompt","type":"string","max_len":4000},{"name":"responsibilities","type":"string","max_len":8000},{"name":"ai_screen_enabled","type":"bool"},{"name":"hiring_manager_id","type":"string","max_len":64},{"name":"salary_visibility","type":"enum","values":["public","team","private"]},{"name":"target_close_date","type":"string","max_len":32},{"name":"external_apply_url","type":"url","max_len":2048},{"name":"knockout_questions","type":"list"},{"name":"screening_questions","type":"list"}]},"message":{"ops":["list","read","create","update","delete"],"create_fields":["candidate_id","application_id","channel","direction","subject","body","status","sent_at","delivered_at","read_at","template_id","from_address","to_address","cc_addresses","thread_id","error"],"update_fields":["subject","body","status","sent_at","delivered_at","read_at","thread_id","error"],"allowed_filters":["data__candidate_id","data__application_id","data__channel","data__status","data__template_id","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__sent_at"],"default_sort":"-data__sent_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":16000},{"name":"error","type":"string","max_len":600},{"name":"status","type":"enum","values":["draft","queued","sent","delivered","failed","bounced"]},{"name":"channel","type":"enum","values":["email","note"]},{"name":"read_at","type":"string","max_len":32},{"name":"sent_at","type":"string","max_len":32},{"name":"subject","type":"string","max_len":400},{"name":"direction","type":"enum","values":["outbound","inbound"]},{"name":"thread_id","type":"string","max_len":200},{"name":"to_address","type":"string","max_len":320},{"name":"template_id","type":"string","max_len":64,"ref":{"type":"email_template","owned":false,"optional":false}},{"name":"candidate_id","type":"string","max_len":64},{"name":"cc_addresses","type":"list"},{"name":"delivered_at","type":"string","max_len":32},{"name":"from_address","type":"string","max_len":320},{"name":"application_id","type":"string","max_len":64}]},"offer":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","candidate_id","job_id","salary_gross","salary_period","currency","bonus","bonus_note","vacation_days","start_date","expires_at","term","term_until","weekly_hours","remote_policy","status","sent_at","decided_at","letter_body","letter_blob_id","decline_reason"],"update_fields":["salary_gross","salary_period","currency","bonus","bonus_note","vacation_days","start_date","expires_at","term","term_until","weekly_hours","remote_policy","status","sent_at","decided_at","letter_body","letter_blob_id","decline_reason"],"allowed_filters":["data__application_id","data__candidate_id","data__job_id","data__status","data__currency","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__start_date","data__sent_at"],"default_sort":"created_at","max_limit":100,"fields":[{"name":"term","type":"enum","values":["permanent","fixed_term","trial","intern","freelance"]},{"name":"bonus","type":"number"},{"name":"job_id","type":"string","max_len":64},{"name":"status","type":"enum","values":["draft","sent","accepted","declined","withdrawn","expired"]},{"name":"sent_at","type":"string","max_len":32},{"name":"currency","type":"string","max_len":8},{"name":"bonus_note","type":"string","max_len":600},{"name":"decided_at","type":"string","max_len":32},{"name":"expires_at","type":"string","max_len":32},{"name":"start_date","type":"string","max_len":32},{"name":"term_until","type":"string","max_len":32},{"name":"letter_body","type":"string","max_len":16000},{"name":"candidate_id","type":"string","max_len":64},{"name":"salary_gross","type":"number"},{"name":"weekly_hours","type":"number"},{"name":"remote_policy","type":"string","max_len":200},{"name":"salary_period","type":"enum","values":["yearly","monthly","daily","hourly"]},{"name":"vacation_days","type":"number"},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"decline_reason","type":"string","max_len":600},{"name":"letter_blob_id","type":"string","max_len":64}]},"source":{"ops":["list","read","create","update","delete"],"create_fields":["name","kind","url","active","notes"],"update_fields":["name","kind","url","active","notes"],"allowed_filters":["data__name","data__kind","data__active","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__name"],"default_sort":"data__name","max_limit":100,"fields":[{"name":"url","type":"url","max_len":2048},{"name":"kind","type":"enum","values":["linkedin","indeed","stepstone","xing","honeypot","kununu","careers_page","referral","active_sourcing","agency","event","other"]},{"name":"name","type":"string","max_len":200},{"name":"notes","type":"string","max_len":2000},{"name":"active","type":"bool"}]},"task":{"ops":["list","read","create","update","delete"],"create_fields":["title","description","due_date","completed","completed_at","priority","assigned_to","parent_kind","parent_id"],"update_fields":["title","description","due_date","completed","completed_at","priority","assigned_to"],"allowed_filters":["data__parent_id","data__parent_kind","data__assigned_to","data__completed","data__priority","status","is_archived","owned_by","created_by"],"allowed_sorts":["data__due_date","created_at","data__priority"],"default_sort":"data__due_date","max_limit":200,"fields":[{"name":"title","type":"string","max_len":200},{"name":"due_date","type":"string","max_len":32},{"name":"priority","type":"enum","values":["low","normal","high","urgent"]},{"name":"completed","type":"bool"},{"name":"parent_id","type":"string","max_len":64},{"name":"assigned_to","type":"string","max_len":64},{"name":"description","type":"string","max_len":4000},{"name":"parent_kind","type":"enum","values":["candidate","application","job"]},{"name":"completed_at","type":"string","max_len":32}]}}` // ── Configuration ────────────────────────────────────────────────── // ListOpts mirrors the standard query parameters the list endpoints // accept. Filters carries arbitrary additional ?key=value pairs. type ListOpts struct { Limit int Offset int Sort string Q string Filters map[string]any } // Client is the per-app HTTP client. Reuse across requests; safe for // concurrent use. type Client struct { HTTPClient *http.Client BaseURL string Token string once sync.Once deviceID string sessID string } // New returns a Client wired to the host this library was generated // against. Pass a personal access token; an empty string falls back to // the XCLIENT_TOKEN environment variable. func New(token string) *Client { base := os.Getenv("XCLIENT_BASE_URL") if base == "" { base = defaultBase } if token == "" { token = os.Getenv("XCLIENT_TOKEN") } return &Client{ HTTPClient: &http.Client{Timeout: 30 * time.Second}, BaseURL: strings.TrimRight(base, "/"), Token: token, } } func (c *Client) initIDs() { c.once.Do(func() { c.deviceID = loadOrMintDeviceID() c.sessID = mintUUID() }) } // ── Identifier persistence ───────────────────────────────────────── func stateDir() string { home, err := os.UserHomeDir() if err != nil || home == "" { return "" } d := filepath.Join(home, "."+ModuleName) _ = os.MkdirAll(d, 0o700) return d } func mintUUID() string { var b [16]byte _, _ = rand.Read(b[:]) b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 h := hex.EncodeToString(b[:]) return h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:32] } func loadOrMintDeviceID() string { dir := stateDir() if dir == "" { return mintUUID() } f := filepath.Join(dir, "device.json") if raw, err := os.ReadFile(f); err == nil { var blob struct { DeviceID string `json:"device_id"` } if json.Unmarshal(raw, &blob) == nil && len(blob.DeviceID) >= 32 { return blob.DeviceID } } id := mintUUID() body, _ := json.Marshal(map[string]string{"device_id": id}) _ = os.WriteFile(f, body, 0o600) return id } // ── Telemetry toggles ────────────────────────────────────────────── func autoupdateEnabled() bool { v := strings.ToLower(os.Getenv("XCLIENT_NO_AUTOUPDATE")) return v != "1" && v != "true" && v != "yes" } // ── Editor / runtime fingerprint ─────────────────────────────────── func fingerprint() map[string]any { out := map[string]any{ "go_version": runtime.Version(), "os": runtime.GOOS, "arch": runtime.GOARCH, } out["term_program"] = os.Getenv("TERM_PROGRAM") out["editor_env"] = os.Getenv("EDITOR") out["ci"] = os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" out["claude_code"] = os.Getenv("CLAUDECODE") != "" || os.Getenv("CLAUDE_CODE_ENTRYPOINT") != "" out["codex"] = os.Getenv("CODEX_HOME") != "" tp := strings.ToLower(os.Getenv("TERM_PROGRAM")) out["vscode"] = tp == "vscode" && os.Getenv("CURSOR_TRACE_ID") == "" out["cursor"] = os.Getenv("CURSOR_TRACE_ID") != "" out["antigravity"] = os.Getenv("ANTIGRAVITY_TRACE_ID") != "" out["jetbrains"] = strings.Contains(tp, "jetbrains") return out } // ── HTTP transport ───────────────────────────────────────────────── // APIError wraps a non-2xx response. type APIError struct { Status int Message string Body any } func (e *APIError) Error() string { return fmt.Sprintf("HTTP %d: %s", e.Status, e.Message) } var retryableStatus = map[int]struct{}{ 408: {}, 425: {}, 429: {}, 500: {}, 502: {}, 503: {}, 504: {}, } func backoff(attempt int, retryAfterSec float64) time.Duration { if retryAfterSec >= 0 { if retryAfterSec > 60 { retryAfterSec = 60 } return time.Duration(retryAfterSec * float64(time.Second)) } delay := float64(int(1) << uint(attempt)) if delay > 60 { delay = 60 } return time.Duration(delay * float64(time.Second)) } func (c *Client) userAgent() string { return fmt.Sprintf("%s/%s (lib/%s; go/%s; %s)", ModuleName, ClientVersion, Language, runtime.Version(), runtime.GOOS) } // requestJSON fires one method+path against the API, JSON in / JSON // out. Pass nil body for read-only verbs. func (c *Client) requestJSON(method, path string, body any) (map[string]any, error) { c.maybeAutoupdate() c.initIDs() u := c.BaseURL + path var data []byte if body != nil { var err error data, err = json.Marshal(body) if err != nil { return nil, err } } const maxRetries = 3 var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { var bodyReader io.Reader if data != nil { bodyReader = bytes.NewReader(data) } req, err := http.NewRequest(method, u, bodyReader) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", c.userAgent()) req.Header.Set("X-Client-Channel", "client_"+Language) req.Header.Set("X-Client-Version", ClientVersion) req.Header.Set("X-Analytics-Device-Id", c.deviceID) req.Header.Set("X-Analytics-Session-Id", c.sessID) if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } if data != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.HTTPClient.Do(req) if err != nil { lastErr = err if attempt+1 < maxRetries { time.Sleep(backoff(attempt, -1)) continue } c.emitCallEvent(method, path, 0, false) return nil, &APIError{Status: 0, Message: err.Error()} } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() c.maybePersistRefresh(resp.Header) if _, retry := retryableStatus[resp.StatusCode]; retry && attempt+1 < maxRetries { ra := -1.0 if v := resp.Header.Get("Retry-After"); v != "" { if f, err2 := strconv.ParseFloat(v, 64); err2 == nil { ra = f } } time.Sleep(backoff(attempt, ra)) continue } if resp.StatusCode >= 400 { var parsed any _ = json.Unmarshal(raw, &parsed) msg := http.StatusText(resp.StatusCode) if m, ok := parsed.(map[string]any); ok { if d, ok2 := m["detail"].(string); ok2 { msg = d } else if d, ok2 := m["message"].(string); ok2 { msg = d } } c.emitCallEvent(method, path, resp.StatusCode, false) return nil, &APIError{Status: resp.StatusCode, Message: msg, Body: parsed} } c.emitCallEvent(method, path, resp.StatusCode, true) if len(raw) == 0 { return nil, nil } var out map[string]any if err := json.Unmarshal(raw, &out); err != nil { return nil, err } return out, nil } if lastErr != nil { c.emitCallEvent(method, path, 0, false) return nil, &APIError{Status: 0, Message: lastErr.Error()} } return nil, errors.New("request failed") } // requestList wraps requestJSON for list endpoints, lifting *ListOpts // into the query string. func (c *Client) requestList(path string, opts *ListOpts) (map[string]any, error) { q := url.Values{} if opts != nil { if opts.Limit > 0 { q.Set("limit", strconv.Itoa(opts.Limit)) } if opts.Offset > 0 { q.Set("offset", strconv.Itoa(opts.Offset)) } if opts.Sort != "" { q.Set("sort", opts.Sort) } if opts.Q != "" { q.Set("q", opts.Q) } for k, v := range opts.Filters { if v == nil { continue } q.Set(k, fmt.Sprint(v)) } } if encoded := q.Encode(); encoded != "" { path = path + "?" + encoded } return c.requestJSON("GET", path, nil) } func (c *Client) maybePersistRefresh(h http.Header) { if v := h.Get("x-auth-refresh-token"); v != "" { c.Token = v } } // ── Analytics ────────────────────────────────────────────────────── var metaSentOnce sync.Once func (c *Client) emitCallEvent(method, pathStr string, status int, ok bool) { go func() { defer func() { _ = recover() }() meta := map[string]any{ "channel": "client_" + Language, "client_version": ClientVersion, "module_name": ModuleName, "language": Language, "os": runtime.GOOS, "go_version": runtime.Version(), } var addEnv bool metaSentOnce.Do(func() { addEnv = true }) if addEnv { meta["env"] = fingerprint() } evt := map[string]any{ "type": "client.call", "ts_client": time.Now().Unix(), "meta": map[string]any{ "method": strings.ToUpper(method), "path": strings.SplitN(pathStr, "?", 2)[0], "status": status, "ok": ok, }, } body := map[string]any{ "device_id": c.deviceID, "session_id": c.sessID, "events": []any{evt}, "meta": meta, } raw, _ := json.Marshal(body) req, err := http.NewRequest("POST", c.BaseURL+"/xapi2/analytics/challenge", bytes.NewReader(raw)) if err != nil { return } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", c.userAgent()) hc := &http.Client{Timeout: 4 * time.Second} resp, err := hc.Do(req) if err != nil { return } _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() }() } // ── Auto-update ──────────────────────────────────────────────────── var autoupdateOnce sync.Once func (c *Client) maybeAutoupdate() { autoupdateOnce.Do(func() { if !autoupdateEnabled() { return } go c.runAutoupdate() }) } func (c *Client) runAutoupdate() { defer func() { _ = recover() }() dir := stateDir() if dir == "" { return } stamp := filepath.Join(dir, "update_check.json") if raw, err := os.ReadFile(stamp); err == nil { var blob struct { CheckedAt int64 `json:"checked_at"` } if json.Unmarshal(raw, &blob) == nil { if time.Now().Unix()-blob.CheckedAt < 86400 { return } } } hc := &http.Client{Timeout: 6 * time.Second} resp, err := hc.Get(c.BaseURL + "/xapi2/clients/version") if err != nil { return } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() var payload struct { Version string `json:"version"` } if json.Unmarshal(raw, &payload) != nil { return } stampBody, _ := json.Marshal(map[string]any{"checked_at": time.Now().Unix()}) _ = os.WriteFile(stamp, stampBody, 0o600) if payload.Version == "" || payload.Version == ClientVersion { return } // Source replacement is intentionally a no-op in Go - the user is // running a compiled binary, the .go file on disk is just a record // of the version they vendored. Surface the new version through // the next build. } // ApplicationList lists application rows. Pass nil opts for defaults. func (c *Client) ApplicationList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/application", opts) } // ApplicationGet fetches one application row by id. func (c *Client) ApplicationGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/application/"+id, nil) } // ApplicationCreate creates a new application row. func (c *Client) ApplicationCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/application", data) } // ApplicationUpdate patches an existing application row. func (c *Client) ApplicationUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/application/"+id, data) } // ApplicationDelete deletes a application row. func (c *Client) ApplicationDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/application/"+id, nil) return err } // ApplicationNoteList lists application_note rows. Pass nil opts for defaults. func (c *Client) ApplicationNoteList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/application_note", opts) } // ApplicationNoteGet fetches one application_note row by id. func (c *Client) ApplicationNoteGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/application_note/"+id, nil) } // ApplicationNoteCreate creates a new application_note row. func (c *Client) ApplicationNoteCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/application_note", data) } // ApplicationNoteUpdate patches an existing application_note row. func (c *Client) ApplicationNoteUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/application_note/"+id, data) } // ApplicationNoteDelete deletes a application_note row. func (c *Client) ApplicationNoteDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/application_note/"+id, nil) return err } // CandidateList lists candidate rows. Pass nil opts for defaults. func (c *Client) CandidateList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/candidate", opts) } // CandidateGet fetches one candidate row by id. func (c *Client) CandidateGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/candidate/"+id, nil) } // CandidateCreate creates a new candidate row. func (c *Client) CandidateCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/candidate", data) } // CandidateUpdate patches an existing candidate row. func (c *Client) CandidateUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/candidate/"+id, data) } // CandidateDelete deletes a candidate row. func (c *Client) CandidateDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/candidate/"+id, nil) return err } // EmailTemplateList lists email_template rows. Pass nil opts for defaults. func (c *Client) EmailTemplateList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/email_template", opts) } // EmailTemplateGet fetches one email_template row by id. func (c *Client) EmailTemplateGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/email_template/"+id, nil) } // EmailTemplateCreate creates a new email_template row. func (c *Client) EmailTemplateCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/email_template", data) } // EmailTemplateUpdate patches an existing email_template row. func (c *Client) EmailTemplateUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/email_template/"+id, data) } // EmailTemplateDelete deletes a email_template row. func (c *Client) EmailTemplateDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/email_template/"+id, nil) return err } // EvaluationList lists evaluation rows. Pass nil opts for defaults. func (c *Client) EvaluationList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/evaluation", opts) } // EvaluationGet fetches one evaluation row by id. func (c *Client) EvaluationGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/evaluation/"+id, nil) } // EvaluationCreate creates a new evaluation row. func (c *Client) EvaluationCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/evaluation", data) } // EvaluationUpdate patches an existing evaluation row. func (c *Client) EvaluationUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/evaluation/"+id, data) } // EvaluationDelete deletes a evaluation row. func (c *Client) EvaluationDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/evaluation/"+id, nil) return err } // InterviewList lists interview rows. Pass nil opts for defaults. func (c *Client) InterviewList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/interview", opts) } // InterviewGet fetches one interview row by id. func (c *Client) InterviewGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/interview/"+id, nil) } // InterviewCreate creates a new interview row. func (c *Client) InterviewCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/interview", data) } // InterviewUpdate patches an existing interview row. func (c *Client) InterviewUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/interview/"+id, data) } // InterviewDelete deletes a interview row. func (c *Client) InterviewDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/interview/"+id, nil) return err } // JobList lists job rows. Pass nil opts for defaults. func (c *Client) JobList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/job", opts) } // JobGet fetches one job row by id. func (c *Client) JobGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/job/"+id, nil) } // JobCreate creates a new job row. func (c *Client) JobCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/job", data) } // JobUpdate patches an existing job row. func (c *Client) JobUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/job/"+id, data) } // JobDelete deletes a job row. func (c *Client) JobDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/job/"+id, nil) return err } // MessageList lists message rows. Pass nil opts for defaults. func (c *Client) MessageList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/message", opts) } // MessageGet fetches one message row by id. func (c *Client) MessageGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/message/"+id, nil) } // MessageCreate creates a new message row. func (c *Client) MessageCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/message", data) } // MessageUpdate patches an existing message row. func (c *Client) MessageUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/message/"+id, data) } // MessageDelete deletes a message row. func (c *Client) MessageDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/message/"+id, nil) return err } // OfferList lists offer rows. Pass nil opts for defaults. func (c *Client) OfferList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/offer", opts) } // OfferGet fetches one offer row by id. func (c *Client) OfferGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/offer/"+id, nil) } // OfferCreate creates a new offer row. func (c *Client) OfferCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/offer", data) } // OfferUpdate patches an existing offer row. func (c *Client) OfferUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/offer/"+id, data) } // OfferDelete deletes a offer row. func (c *Client) OfferDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/offer/"+id, nil) return err } // SourceList lists source rows. Pass nil opts for defaults. func (c *Client) SourceList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/source", opts) } // SourceGet fetches one source row by id. func (c *Client) SourceGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/source/"+id, nil) } // SourceCreate creates a new source row. func (c *Client) SourceCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/source", data) } // SourceUpdate patches an existing source row. func (c *Client) SourceUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/source/"+id, data) } // SourceDelete deletes a source row. func (c *Client) SourceDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/source/"+id, nil) return err } // TaskList lists task rows. Pass nil opts for defaults. func (c *Client) TaskList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/task", opts) } // TaskGet fetches one task row by id. func (c *Client) TaskGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/task/"+id, nil) } // TaskCreate creates a new task row. func (c *Client) TaskCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/task", data) } // TaskUpdate patches an existing task row. func (c *Client) TaskUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/task/"+id, data) } // TaskDelete deletes a task row. func (c *Client) TaskDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/task/"+id, nil) return err }