// Drop-in C# / .NET client library for the KI BMS HTTP API. // // Save this file under your project as `AtsClient.cs`, in a // directory matching `namespace ats_client;`, then call the // AtsClient class: // // using ats_client; // var c = new AtsClient("pat_..."); // var rows = await c.AccountListAsync(new ListOpts { Limit = 20 }); // var fresh = await c.AccountCreateAsync(new Dictionary { ["name"] = "Example GmbH" }); // // Every endpoint exposed by the HTTP API is wrapped as an // `Async` method on AtsClient. List methods take ListOpts; // get/update/delete methods take the row id as their first argument. // // Provided as-is, with no warranty. Vendor freely; modify as needed. // Targets .NET 6+; uses only HttpClient and System.Text.Json from the // BCL. No NuGet packages required. // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. // Local edits will be overwritten by the once-per-day version check. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; namespace ats_client; /// API client wrapper for KI BMS. public sealed class AtsClient { public const string AppSlug = "ats"; public const string AppName = "KI BMS"; public const string ModuleName = "ats_client"; public const string ClientVersion = "0.3.13"; public const string Language = "csharp"; private const string DefaultBase = "https://www.ki-bewerber-management.de"; /// Per-type metadata baked at generation time; parse with /// JsonNode.Parse if you need the legal filters / sorts / max_limit /// for a model without an extra round-trip. public const string 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}]}}"; private readonly HttpClient _http; private string _baseUrl; private string _token; private readonly string _deviceId; private readonly string _sessionId; private static int _metaSentOnce = 0; private static int _autoupdateTried = 0; public AtsClient(string? token = null) { var envBase = Environment.GetEnvironmentVariable("XCLIENT_BASE_URL"); _baseUrl = (string.IsNullOrEmpty(envBase) ? DefaultBase : envBase!).TrimEnd('/'); if (string.IsNullOrEmpty(token)) { _token = Environment.GetEnvironmentVariable("XCLIENT_TOKEN") ?? ""; } else { _token = token!; } // We follow redirects manually so we can drop Authorization on // cross-origin hops; AllowAutoRedirect=false is essential. var handler = new HttpClientHandler { AllowAutoRedirect = false }; _http = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) }; _deviceId = LoadOrMintDeviceId(); _sessionId = Guid.NewGuid().ToString(); } public void SetToken(string? token) { _token = token ?? ""; } public void SetBaseUrl(string baseUrl){ _baseUrl = (baseUrl ?? "").TrimEnd('/'); } // ── Identifier persistence ─────────────────────────────────────── private static string? StateDir() { var home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE"); if (string.IsNullOrEmpty(home)) return null; var d = Path.Combine(home!, "." + ModuleName); try { Directory.CreateDirectory(d); } catch { return null; } return d; } private static string LoadOrMintDeviceId() { var d = StateDir(); if (d == null) return Guid.NewGuid().ToString(); var f = Path.Combine(d, "device.json"); if (File.Exists(f)) { try { var raw = File.ReadAllText(f); var node = JsonNode.Parse(raw); var did = node?["device_id"]?.GetValue(); if (!string.IsNullOrEmpty(did) && did!.Length >= 32) return did!; } catch { /* fall through to mint */ } } var fresh = Guid.NewGuid().ToString(); try { File.WriteAllText(f, "{\"device_id\":\"" + fresh + "\"}"); } catch { /* best-effort */ } return fresh; } private static bool AutoupdateEnabled() { var v = (Environment.GetEnvironmentVariable("XCLIENT_NO_AUTOUPDATE") ?? "").ToLowerInvariant(); return v != "1" && v != "true" && v != "yes"; } // ── Editor / runtime fingerprint ───────────────────────────────── private static Dictionary Fingerprint() { var tp = (Environment.GetEnvironmentVariable("TERM_PROGRAM") ?? "").ToLowerInvariant(); return new Dictionary { ["dotnet_version"] = Environment.Version.ToString(), ["os"] = Environment.OSVersion.Platform.ToString(), ["term_program"] = Environment.GetEnvironmentVariable("TERM_PROGRAM"), ["editor_env"] = Environment.GetEnvironmentVariable("EDITOR"), ["ci"] = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")), ["claude_code"] = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CLAUDECODE")) || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CLAUDE_CODE_ENTRYPOINT")), ["codex"] = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CODEX_HOME")), ["vscode"] = tp == "vscode" && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CURSOR_TRACE_ID")), ["cursor"] = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CURSOR_TRACE_ID")), ["antigravity"] = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ANTIGRAVITY_TRACE_ID")), ["jetbrains"] = tp.Contains("jetbrains"), }; } // ── HTTP transport ─────────────────────────────────────────────── public sealed class ApiException : Exception { public int Status { get; } public string? BodyRaw { get; } public ApiException(int status, string message, string? body = null) : base("HTTP " + status + ": " + message) { Status = status; BodyRaw = body; } } public sealed class ListOpts { public int? Limit { get; set; } public int? Offset { get; set; } public string? Sort { get; set; } public string? Q { get; set; } public Dictionary? Filters { get; set; } } private static readonly HashSet _retryable = new() { 408, 425, 429, 500, 502, 503, 504 }; private const int _maxRetries = 3; public async Task?> RequestListAsync(string path, ListOpts? opts, CancellationToken ct = default) { var qs = new List(); if (opts != null) { if (opts.Limit is int l) qs.Add("limit=" + l); if (opts.Offset is int o) qs.Add("offset=" + o); if (!string.IsNullOrEmpty(opts.Sort)) qs.Add("sort=" + Uri.EscapeDataString(opts.Sort!)); if (!string.IsNullOrEmpty(opts.Q)) qs.Add("q=" + Uri.EscapeDataString(opts.Q!)); if (opts.Filters != null) { foreach (var kv in opts.Filters) { if (kv.Value == null) continue; qs.Add(Uri.EscapeDataString(kv.Key) + "=" + Uri.EscapeDataString(kv.Value.ToString() ?? "")); } } } if (qs.Count > 0) path += (path.Contains('?') ? "&" : "?") + string.Join("&", qs); return await RequestJsonAsync("GET", path, null, ct).ConfigureAwait(false); } public async Task?> RequestJsonAsync(string method, string path, object? body, CancellationToken ct = default) { MaybeAutoupdate(); var url = _baseUrl + path; string? json = body == null ? null : JsonSerializer.Serialize(body); Exception? lastErr = null; for (int attempt = 0; attempt < _maxRetries; attempt++) { HttpResponseMessage? resp = null; try { resp = await SendFollowingRedirectsAsync(method, url, json, ct).ConfigureAwait(false); if (resp.Headers.TryGetValues("x-auth-refresh-token", out var fresh)) { var f = fresh.FirstOrDefault(); if (!string.IsNullOrEmpty(f)) _token = f!; } int status = (int)resp.StatusCode; if (_retryable.Contains(status) && attempt + 1 < _maxRetries) { double? ra = null; if (resp.Headers.TryGetValues("Retry-After", out var raVals)) { if (double.TryParse(raVals.FirstOrDefault(), out var raD)) ra = raD; } await Task.Delay(BackoffMs(attempt, ra), ct).ConfigureAwait(false); continue; } var raw = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); if (status >= 400) { EmitCallEvent(method, path, status, false); throw new ApiException(status, ErrorMessage(raw, resp.ReasonPhrase ?? "request failed"), raw); } EmitCallEvent(method, path, status, true); if (string.IsNullOrEmpty(raw)) return null; return DecodeObject(raw); } catch (HttpRequestException e) { lastErr = e; if (attempt + 1 < _maxRetries) { await Task.Delay(BackoffMs(attempt, null), ct).ConfigureAwait(false); continue; } EmitCallEvent(method, path, 0, false); throw new ApiException(0, e.Message); } catch (TaskCanceledException e) { lastErr = e; if (attempt + 1 < _maxRetries) { await Task.Delay(BackoffMs(attempt, null), ct).ConfigureAwait(false); continue; } EmitCallEvent(method, path, 0, false); throw new ApiException(0, "request timed out"); } finally { resp?.Dispose(); } } EmitCallEvent(method, path, 0, false); throw new ApiException(0, lastErr?.Message ?? "request failed"); } /// Walk redirects manually so we can strip Authorization /// when the next hop targets a different origin. Caps at 5 hops; /// follows RFC 7231 method-rewrite rules. private async Task SendFollowingRedirectsAsync(string method, string url, string? json, CancellationToken ct) { var currentUrl = url; var currentMethod = method.ToUpperInvariant(); var currentJson = json; var stripAuth = false; const int maxHops = 5; for (int hop = 0; hop <= maxHops; hop++) { using var req = new HttpRequestMessage(new HttpMethod(currentMethod), currentUrl); req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); req.Headers.UserAgent.ParseAdd(UserAgent()); req.Headers.Add("X-Client-Channel", "client_" + Language); req.Headers.Add("X-Client-Version", ClientVersion); req.Headers.Add("X-Analytics-Device-Id", _deviceId); req.Headers.Add("X-Analytics-Session-Id", _sessionId); if (!stripAuth && !string.IsNullOrEmpty(_token)) { req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); } if (currentJson != null && currentMethod != "GET" && currentMethod != "HEAD") { req.Content = new StringContent(currentJson, Encoding.UTF8, "application/json"); } var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); int status = (int)resp.StatusCode; if (status < 300 || status >= 400 || status == 304 || hop == maxHops) return resp; var loc = resp.Headers.Location; if (loc == null) return resp; Uri nextUri; try { nextUri = loc.IsAbsoluteUri ? loc : new Uri(new Uri(currentUrl), loc); } catch { return resp; } try { var cur = new Uri(currentUrl); if (!string.Equals(cur.GetLeftPart(UriPartial.Authority), nextUri.GetLeftPart(UriPartial.Authority), StringComparison.OrdinalIgnoreCase)) { stripAuth = true; } } catch { /* default keeps stripAuth as-is */ } if (status == 303 || ((status == 301 || status == 302) && currentMethod != "GET" && currentMethod != "HEAD")) { currentMethod = "GET"; currentJson = null; } currentUrl = nextUri.ToString(); resp.Dispose(); } throw new HttpRequestException("too many redirects"); } private static int BackoffMs(int attempt, double? retryAfter) { if (retryAfter is double r && r >= 0) return (int)(Math.Min(r, 60.0) * 1000.0); return (int)(Math.Min(Math.Pow(2, attempt), 60.0) * 1000.0); } private static string ErrorMessage(string body, string fallback) { if (string.IsNullOrEmpty(body) || body[0] != '{') return fallback; try { var node = JsonNode.Parse(body); return node?["detail"]?.GetValue() ?? node?["message"]?.GetValue() ?? fallback; } catch { return fallback; } } private static Dictionary DecodeObject(string raw) { try { var node = JsonNode.Parse(raw); if (node is JsonObject obj) return ToDict(obj); return new Dictionary { ["data"] = ToBoxed(node) }; } catch { return new Dictionary { ["data"] = raw }; } } private static Dictionary ToDict(JsonObject obj) { var d = new Dictionary(); foreach (var kv in obj) d[kv.Key] = ToBoxed(kv.Value); return d; } private static object? ToBoxed(JsonNode? n) { if (n == null) return null; if (n is JsonObject o) return ToDict(o); if (n is JsonArray a) { var l = new List(); foreach (var i in a) l.Add(ToBoxed(i)); return l; } if (n is JsonValue v) { if (v.TryGetValue(out var b)) return b; if (v.TryGetValue(out var ll)) return ll; if (v.TryGetValue(out var dd)) return dd; return v.ToString(); } return n.ToString(); } private static string UserAgent() { return ModuleName + "/" + ClientVersion + " (lib/" + Language + "; dotnet/" + Environment.Version + ")"; } // ── Analytics ──────────────────────────────────────────────────── private void EmitCallEvent(string method, string path, int status, bool ok) { // Run on the thread pool so the calling request returns // immediately - the analytics ping has its own 4 s timeout and // never feeds back into the caller. _ = Task.Run(async () => { try { bool includeEnv = Interlocked.CompareExchange(ref _metaSentOnce, 1, 0) == 0; var meta = new Dictionary { ["channel"] = "client_" + Language, ["client_version"] = ClientVersion, ["module_name"] = ModuleName, ["language"] = Language, ["os"] = Environment.OSVersion.Platform.ToString(), ["dotnet_version"] = Environment.Version.ToString(), }; if (includeEnv) meta["env"] = Fingerprint(); var evt = new Dictionary { ["type"] = "client.call", ["ts_client"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), ["meta"] = new Dictionary { ["method"] = method.ToUpperInvariant(), ["path"] = path.Split('?', 2)[0], ["status"] = status, ["ok"] = ok, }, }; var body = new Dictionary { ["device_id"] = _deviceId, ["session_id"] = _sessionId, ["events"] = new[] { evt }, ["meta"] = meta, }; using var ping = new HttpClient { Timeout = TimeSpan.FromSeconds(4) }; using var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); using var resp = await ping.PostAsync(_baseUrl + "/xapi2/analytics/challenge", content).ConfigureAwait(false); } catch { /* fire and forget */ } }); } // ── Auto-update ────────────────────────────────────────────────── private void MaybeAutoupdate() { if (Interlocked.CompareExchange(ref _autoupdateTried, 1, 0) != 0) return; if (!AutoupdateEnabled()) return; // Source replacement is intentionally a no-op - the user is // running compiled IL, the .cs file is just a record of the // version they vendored. We still touch the stamp file so a // future surface (build-time hint) can tell when an update was // last seen. _ = Task.Run(async () => { try { var d = StateDir(); if (d == null) return; var stamp = Path.Combine(d, "update_check.json"); long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (File.Exists(stamp)) { try { var raw = await File.ReadAllTextAsync(stamp).ConfigureAwait(false); var node = JsonNode.Parse(raw); if (node?["checked_at"]?.GetValue() is long last && now - last < 86400) return; } catch { /* fall through */ } } await File.WriteAllTextAsync(stamp, "{\"checked_at\":" + now + "}").ConfigureAwait(false); } catch { /* best-effort */ } }); } // ── Generated per-type wrapper methods ─────────────────────────── // Every model that exposes an op gets one `Async` method // below. The runtime above does the heavy lifting; these wrappers // just pin the URL + HTTP verb. public Task?> ApplicationListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/application", opts, ct); public Task?> ApplicationGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/application/" + id, null, ct); public Task?> ApplicationCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/application", data, ct); public Task?> ApplicationUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/application/" + id, data, ct); public async Task ApplicationDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/application/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> ApplicationNoteListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/application_note", opts, ct); public Task?> ApplicationNoteGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/application_note/" + id, null, ct); public Task?> ApplicationNoteCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/application_note", data, ct); public Task?> ApplicationNoteUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/application_note/" + id, data, ct); public async Task ApplicationNoteDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/application_note/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> CandidateListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/candidate", opts, ct); public Task?> CandidateGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/candidate/" + id, null, ct); public Task?> CandidateCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/candidate", data, ct); public Task?> CandidateUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/candidate/" + id, data, ct); public async Task CandidateDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/candidate/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> EmailTemplateListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/email_template", opts, ct); public Task?> EmailTemplateGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/email_template/" + id, null, ct); public Task?> EmailTemplateCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/email_template", data, ct); public Task?> EmailTemplateUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/email_template/" + id, data, ct); public async Task EmailTemplateDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/email_template/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> EvaluationListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/evaluation", opts, ct); public Task?> EvaluationGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/evaluation/" + id, null, ct); public Task?> EvaluationCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/evaluation", data, ct); public Task?> EvaluationUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/evaluation/" + id, data, ct); public async Task EvaluationDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/evaluation/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> InterviewListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/interview", opts, ct); public Task?> InterviewGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/interview/" + id, null, ct); public Task?> InterviewCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/interview", data, ct); public Task?> InterviewUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/interview/" + id, data, ct); public async Task InterviewDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/interview/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> JobListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/job", opts, ct); public Task?> JobGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/job/" + id, null, ct); public Task?> JobCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/job", data, ct); public Task?> JobUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/job/" + id, data, ct); public async Task JobDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/job/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> MessageListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/message", opts, ct); public Task?> MessageGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/message/" + id, null, ct); public Task?> MessageCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/message", data, ct); public Task?> MessageUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/message/" + id, data, ct); public async Task MessageDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/message/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> OfferListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/offer", opts, ct); public Task?> OfferGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/offer/" + id, null, ct); public Task?> OfferCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/offer", data, ct); public Task?> OfferUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/offer/" + id, data, ct); public async Task OfferDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/offer/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> SourceListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/source", opts, ct); public Task?> SourceGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/source/" + id, null, ct); public Task?> SourceCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/source", data, ct); public Task?> SourceUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/source/" + id, data, ct); public async Task SourceDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/source/" + id, null, ct).ConfigureAwait(false); return true; } public Task?> TaskListAsync(ListOpts? opts = null, CancellationToken ct = default) => RequestListAsync("/xapi2/data/task", opts, ct); public Task?> TaskGetAsync(string id, CancellationToken ct = default) => RequestJsonAsync("GET", "/xapi2/data/task/" + id, null, ct); public Task?> TaskCreateAsync(IDictionary data, CancellationToken ct = default) => RequestJsonAsync("POST", "/xapi2/data/task", data, ct); public Task?> TaskUpdateAsync(string id, IDictionary data, CancellationToken ct = default) => RequestJsonAsync("PATCH", "/xapi2/data/task/" + id, data, ct); public async Task TaskDeleteAsync(string id, CancellationToken ct = default) { await RequestJsonAsync("DELETE", "/xapi2/data/task/" + id, null, ct).ConfigureAwait(false); return true; } }