Production-ready C# API Client
2026-03-10
A complete, production-ready C# client implementation for noncaptcha.com API with proper error handling, cancellation support, and all captcha types covered.
Why use this client?
Instead of writing HTTP requests manually each time, this reusable client provides:
- Type safety: Strongly-typed requests and responses
- Error handling: Proper exception types with status codes and raw bodies
- Cancellation support: Full
CancellationTokenpropagation - Multipart handling: Correct content-type headers for image uploads
- Resource management: Implements
IDisposablefor HttpClient cleanup
Complete Source Code
Copy and paste this class directly into your project:
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;
}
}
}
Usage Example
How to integrate the client into your application:
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();
}
Best Practices
- Reuse the client: Create a single instance and reuse it throughout your application lifecycle (HttpClient is designed for reuse).
- Cancellation tokens: Always pass cancellation tokens from your application layer to support graceful shutdowns.
-
Error handling:
Catch
NonCaptchaApiExceptionspecifically to handle API errors separately from network issues. -
Timeout configuration:
Adjust
client.Timeoutbased on your captcha type (puzzle/coords may take longer than text).