Готовый SDK

Полноценный C# клиент для production

2026-03-10

Полноценная реализация C# клиента для API noncaptcha.com, готовая к использованию в продакшене: обработка ошибок, поддержка отмены операций и все типы капч.

Почему стоит использовать этот клиент?

Вместо ручного написания HTTP-запросов каждый раз, этот переиспользуемый клиент предоставляет:

  • Типобезопасность: Строго типизированные запросы и ответы
  • Обработка ошибок: Корректные типы исключений со статус-кодами и сырыми телами ответов
  • Поддержка отмены: Полная поддержка CancellationToken
  • Работа с multipart: Корректные заголовки для загрузки изображений
  • Управление ресурсами: Реализует IDisposable для корректного освобождения HttpClient

Полный исходный код

Скопируйте и вставьте этот класс напрямую в ваш проект:

C# Client Implementation
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

namespace NonCaptcha
{
    public class ApiClient : IDisposable
    {
        private readonly HttpClient _http;
        private readonly string _apiKey;
        private readonly JsonSerializerOptions _json;

        public ApiClient(string apiKey, string baseUrl = "http://api.noncaptcha.com")
        {
            if (string.IsNullOrWhiteSpace(baseUrl)) 
                throw new ArgumentException("baseUrl required", nameof(baseUrl));
            if (string.IsNullOrWhiteSpace(apiKey)) 
                throw new ArgumentException("apiKey required", nameof(apiKey));
            
            _apiKey = apiKey;
            _http = new HttpClient();
            _http.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
            _http.Timeout = TimeSpan.FromSeconds(10);
            _json = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        }

        public TimeSpan Timeout
        {
            get => _http.Timeout;
            set => _http.Timeout = value;
        }

        public void Dispose() => _http.Dispose();

        public Task<BalanceResponse> GetBalanceAsync(CancellationToken ct = default)
            => SendJsonAsync<BalanceResponse>(HttpMethod.Get, "api/json/balance", content: null, ct);

        public Task<EndpointsResponse> GetEndpointsAsync(CancellationToken ct = default)
            => SendJsonAsync<EndpointsResponse>(HttpMethod.Get, "api/json/endpoints", content: null, ct);

        public async Task<TextCaptchaResponse> SolveTextAsync(string fileName, CancellationToken ct = default)
        {
            var mp = new MultipartFormDataContent();
            var imageBytes = await File.ReadAllBytesAsync(fileName, ct);
            var imageContent = new ByteArrayContent(imageBytes);
            imageContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
            mp.Add(imageContent, "image", Path.GetFileName(fileName));
            
            return await SendJsonAsync<TextCaptchaResponse>(
                HttpMethod.Post, "api/json/text", mp, ct
            );
        }

        public async Task<PuzzleCaptchaResponse> SolvePuzzleAsync(
            string fileName, string task, CancellationToken ct = default)
        {
            if (string.IsNullOrWhiteSpace(task)) 
                throw new ArgumentException("task required", nameof(task));
            if (string.IsNullOrWhiteSpace(fileName)) 
                throw new ArgumentException("fileName required", nameof(fileName));
            
            var mp = new MultipartFormDataContent();
            var imageBytes = await File.ReadAllBytesAsync(fileName, ct);
            var imageContent = new ByteArrayContent(imageBytes);
            imageContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
            mp.Add(imageContent, "captcha", Path.GetFileName(fileName));
            mp.Add(new StringContent(task, Encoding.UTF8), "task");
            
            return await SendJsonAsync<PuzzleCaptchaResponse>(
                HttpMethod.Post, "api/json/puzzle", mp, ct
            );
        }

        public async Task<CoordsCaptchaResponse> SolveCoordsAsync(
            string captchaFileName, string hintFileName, CancellationToken ct = default)
        {
            if (string.IsNullOrWhiteSpace(captchaFileName)) 
                throw new ArgumentException("captchaFileName required", nameof(captchaFileName));
            if (string.IsNullOrWhiteSpace(hintFileName)) 
                throw new ArgumentException("hintFileName required", nameof(hintFileName));
            
            var mp = new MultipartFormDataContent();
            
            var captchaBytes = await File.ReadAllBytesAsync(captchaFileName, ct);
            var captchaContent = new ByteArrayContent(captchaBytes);
            captchaContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
            mp.Add(captchaContent, "captcha", Path.GetFileName(captchaFileName));
            
            var hintBytes = await File.ReadAllBytesAsync(hintFileName, ct);
            var hintContent = new ByteArrayContent(hintBytes);
            hintContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
            mp.Add(hintContent, "hint", Path.GetFileName(hintFileName));
            
            return await SendJsonAsync<CoordsCaptchaResponse>(
                HttpMethod.Post, "api/json/coords", mp, ct
            );
        }

        private async Task<T> SendJsonAsync<T>(
            HttpMethod method, string relativeUrl, HttpContent? content, CancellationToken ct)
        {
            using var req = new HttpRequestMessage(method, relativeUrl);
            req.Headers.TryAddWithoutValidation("X-API-Key", _apiKey);
            
            if (content != null) 
                req.Content = content;
            
            using var resp = await _http.SendAsync(
                req, HttpCompletionOption.ResponseHeadersRead, ct
            ).ConfigureAwait(false);
            
            var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
            
            if (resp.IsSuccessStatusCode)
            {
                var ok = JsonSerializer.Deserialize<T>(body, _json);
                if (ok == null)
                    throw new NonCaptchaApiException(
                        resp.StatusCode, 
                        new ErrorResponse("INVALID_RESPONSE", "Empty or invalid JSON response"), 
                        body
                    );
                return ok;
            }
            
            ErrorResponse? err = null;
            try { err = JsonSerializer.Deserialize<ErrorResponse>(body, _json); } catch { }
            err ??= new ErrorResponse("HTTP_ERROR", $"HTTP {(int)resp.StatusCode} {resp.ReasonPhrase}");
            
            throw new NonCaptchaApiException(resp.StatusCode, err, body);
        }
    }

    public sealed record BalanceResponse(double balance);
    public sealed record EndpointInfo(string url);
    public sealed record EndpointsResponse(List<EndpointInfo> endpoints);
    public sealed record TextCaptchaResponse(string text);
    public sealed record PuzzleCaptchaResponse(int step);
    public sealed record CoordsCaptchaResponse(List<CoordPoint> coords);
    public sealed record CoordPoint(int x, int y);
    public sealed record ErrorResponse(string error, string message);

    public sealed class NonCaptchaApiException : Exception
    {
        public HttpStatusCode StatusCode { get; }
        public ErrorResponse Error { get; }
        public string? RawBody { get; }

        public NonCaptchaApiException(
            HttpStatusCode statusCode, ErrorResponse error, string? rawBody = null)
            : base($"{(int)statusCode} {statusCode}: {error.error} - {error.message}")
        {
            StatusCode = statusCode;
            Error = error;
            RawBody = rawBody;
        }
    }
}

Пример использования

Как интегрировать клиент в ваше приложение:

C# Usage
// Initialize once (e.g., in DI container or as singleton)
var client = new ApiClient("YOUR_API_KEY_HERE");

try
{
    // Get current balance
    var balance = await client.GetBalanceAsync();
    Console.WriteLine($"Balance: {balance.balance} RUB");

    // Solve text captcha
    var textResult = await client.SolveTextAsync("captcha.png");
    Console.WriteLine($"Solved text: {textResult.text}");

    // Solve puzzle captcha
    var puzzleResult = await client.SolvePuzzleAsync(
        "puzzle.png", 
        "[6,8,2,7,0,6,8,0,3,2,8,3,3,0,1,5,2,7,8,6,6,2,6,1,2,6]"
    );
    Console.WriteLine($"Puzzle step: {puzzleResult.step}");

    // Solve coords captcha
    var coordsResult = await client.SolveCoordsAsync("main.png", "hint.png");
    foreach (var point in coordsResult.coords)
    {
        Console.WriteLine($"Click at: ({point.x}, {point.y})");
    }
}
catch (NonCaptchaApiException ex) when (ex.StatusCode == HttpStatusCode.PaymentRequired)
{
    Console.WriteLine("Insufficient balance!");
}
catch (NonCaptchaApiException ex)
{
    Console.WriteLine($"API error: {ex.Error.error} - {ex.Error.message}");
    Console.WriteLine($"Raw response: {ex.RawBody}");
}
finally
{
    // Always dispose to release HttpClient resources
    client.Dispose();
}

Рекомендации

  • Повторное использование клиента: Создайте один экземпляр и повторно используйте его в течение всего жизненного цикла приложения (HttpClient предназначен для повторного использования).
  • Токены отмены: Всегда передавайте токены отмены из слоя приложения для поддержки корректного завершения работы.
  • Обработка ошибок: Перехватывайте NonCaptchaApiException отдельно для обработки ошибок API, отличных от сетевых проблем.
  • Настройка таймаутов: Настройте client.Timeout в зависимости от типа капчи (пазлы/координаты могут решаться дольше текстовых).