Elinizde bir pasaport fotoğrafı ve adı, numarayı, tarihleri ve makine tarafından okunabilir bölgeyi (MRZ) veri olarak isteyen bir .NET 8 servisi var. Bu yazı o istemciyi düz C# 12 ile kuruyor: HttpClient, System.Net.Http.Json ve System.Text.Json, SDK yok. İlk çağrıyı, yanıt için record'ları, hata olmayan durumları, asla iki kez ücret almayan yeniden denemeleri, koda göre hata yönetimini ve ASP.NET Core için tipli bir istemciyi ele alıyor.
API, doc.cheap'in arkasındaki Pasaport ve Kimlik OCR API'si. Senkron çalışır: bir POST, bir yanıt, sorgulanacak bir job id yok.
Hesap olmadan ilk çağrı
Herkese açık sandbox anahtarı sk_sandbox_public. Kayıt gerektirmez ve kredi düşmez. IP adresi başına saatte 10 istekle ve ömür boyu az sayıda ücretsiz denemeyle sınırlıdır; bunlar bittiğinde 403 registration_required döner.
İstek gövdesi, tek zorunlu alanı olan bir JSON'dur: image, yani dosyanın baytlarının base64 hali. JPEG ve PNG kabul edilir; diğer her şey 415 unsupported_media_type ile reddedilir ve bu kontrol dosyanın adına değil imzasına bakılarak yapılır.
using System.Net.Http.Headers;
using System.Net.Http.Json;
var http = new HttpClient { BaseAddress = new Uri("https://api.doc.cheap/v1/") };
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "sk_sandbox_public");
var image = Convert.ToBase64String(await File.ReadAllBytesAsync("passport.jpg"));
using var response = await http.PostAsJsonAsync("scans", new { image });
Console.WriteLine((int)response.StatusCode);
Console.WriteLine(await response.Content.ReadAsStringAsync());
İstek şeması katıdır. Gövdedeki bilinmeyen bir anahtar 422 validation_failed ile reddedilir; böylece bir seçenek adındaki yazım hatası sessizce yok sayılmak yerine açıkça hata verir.
Ne geri döner
Her yanıtta aynı sekiz grup bulunur ve her anahtar her zaman vardır: meta, document, holder, fields, mrz, images, quality ve authenticity. Kısaltılmış ve değerleri maskelenmiş bir örnek:
{
"meta": {
"schema_version": "1.0",
"id": "<SCAN_ID>",
"status": "recognized",
"billed": true,
"confidence": "high",
"created_at": "<ISO_TIMESTAMP>",
"reference": null
},
"document": {
"kind": "passport",
"country": "<ISO3>",
"number": "<DOCUMENT_NUMBER>",
"expiry_date": "<YYYY-MM-DD>",
"is_expired": false
},
"holder": {
"given_names": "<GIVEN_NAMES>",
"surname": "<SURNAME>",
"birth_date": "<YYYY-MM-DD>",
"sex": "<M|F|X>",
"nationality": "<ISO3>"
},
"mrz": { "status": "passed", "reason": null, "lines": ["<MRZ_LINE_1>", "<MRZ_LINE_2>"], "text": "<MRZ_TEXT>" }
}
Hiçbir şey okunamadığında document ve holder değeri null olur. mrz.status değeri passed, failed veya absent olur; passed, kontrol hanelerinin tuttuğu anlamına gelir.
Yanıt için record'lar
JSON snake_case kullanır. .NET 8, JsonNamingPolicy.SnakeCaseLower ile birlikte gelir; bu sayede record'lar olağan C# adlarını koruyabilir. Bilinmeyen özellikler varsayılan olarak yok sayılır, yani yalnızca kullandığınız alanları tanımlarsınız.
using System.Text.Json;
public static class Api
{
public static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
}
public sealed record Scan(Meta Meta, DocumentInfo? Document, Holder? Holder, Mrz Mrz);
public sealed record Meta(
string Id, string Status, bool Billed, string Confidence,
string CreatedAt, string? Reference);
public sealed record DocumentInfo(
string? Kind, string? Country, string? Number,
string? IssueDate, string? ExpiryDate, bool? IsExpired, int? DaysRemaining);
public sealed record Holder(
string? GivenNames, string? Surname, string? BirthDate,
string? Sex, string? Nationality);
public sealed record Mrz(string Status, string? Reason, IReadOnlyList<string>? Lines, string? Text);
public sealed record ApiError(string Code, string Message, string? DocsUrl, string? RequestId);
public sealed record ErrorEnvelope(ApiError Error);
Tarihler burada bilerek string olarak bırakıldı. Onları ihtiyaç duyduğunuz yerde DateOnly.TryParse ile ayrıştırın; böylece bir belgedeki tek bir tuhaf değer deserializer'ın içinde exception fırlatmaz.
Bulanık fotoğraf bir exception değildir
200 her zaman bir belgenin okunduğu anlamına gelmez. meta.status değerine göre dallanın:
meta.status |
Anlamı | Ne yapılmalı |
|---|---|---|
recognized |
Belge türü belirlendi ve veri çıkarıldı | document, holder, mrz kullanın |
no_document_found |
Karede belgeye benzeyen bir şey yoktu | Yeni bir fotoğraf isteyin |
unreadable |
Bir belge vardı ama kullanılabilir metin okunamadı | Daha net bir fotoğraf isteyin |
unsupported_document |
Bir belge bulundu ama türü bilinen türlerden değil | Kullanıcıya bildirin |
rejected |
Tanıma servis tarafında başarısız oldu | meta.id değerini loglayın ve daha sonra tekrar deneyin |
Kim öder: billed bayrağı
Sandbox anahtarları asla kredi düşmez. Canlı (live) bir anahtarda tarama yalnızca ücretlendirilebilir olduğunda ücretlendirilir: belge türü belirlenmiş ve şunlardan en az biri okunmuş olmalıdır – kontrol haneleri doğrulanan bir MRZ, basılı bölgeden beş veya daha fazla alan ya da çözümlenmiş bir barkod. Aksi halde ayrılan kredi serbest bırakılır. Sandbox anahtarında da meta.billed taramanın bu kuralı karşılayıp karşılamadığını gösterir, ancak hiçbir bakiyeye dokunulmaz.
meta.billed hangisinin olduğunu söyler. Onu kendi siparişinizin ya da kullanıcınızın yanında saklayın; ücretleri tahminle mutabık kılmak zorunda kalmazsınız. Canlı bir hesap ayrıca her ay 100 ücretsiz tanıma alır.
İki kez ücret alamayan yeniden denemeler
Sizin tarafınızdaki bir timeout, taramanın bizim tarafımızda çalışmadığı anlamına gelmez. Bir Idempotency-Key başlığı (1 ile 255 karakter arası) gönderin ve aynı belgenin her yeniden denemesinde onu tekrar kullanın. Canlı bir anahtarda sunucu, anahtarı gövdenin parmak iziyle birlikte eşleştirir:
- Aynı anahtar, aynı gövde: ilk sonuç yeniden oynatılır ve ikinci bir kredi düşmez.
- Aynı anahtar, farklı gövde:
409 idempotency_conflict. Yeni bir anahtar kullanın. - İlk çağrı hâlâ çalışıyor: ikincisi kısa bir süre bekler, ardından
409 idempotency_in_progressalır. Anahtarı koruyun ve yeniden deneyin. - İlk sonuç artık saklanmıyor:
409 idempotency_replay_unavailable. Yeni bir anahtar kullanın.
Kredi ayrıldıktan sonra engine başarısız olursa ayırma serbest bırakılır ve anahtar dürüst bir yeniden deneme için boşa çıkar. Ayrıntılar idempotency ile güvenli yeniden deneme rehberinde.
İstemcinin tamamı
Saklanmaya değer sürüm bu. Yalnızca yeniden denemeye değer kodları yeniden dener, Retry-After değerine 60 saniyeye kadar uyar, denemeler boyunca tek bir anahtar kullanır ve diğer her hatayı kodu taşıyan bir exception'a dönüştürür.
using System.Net.Http.Json;
public sealed class ScanFailed(int status, ApiError error)
: Exception($"{status} {error.Code}: {error.Message}")
{
public int Status { get; } = status;
public ApiError Error { get; } = error;
}
public sealed class PassportClient(HttpClient http)
{
static readonly HashSet<string> Retryable =
[
"rate_limited", "document_repeated", "internal_error", "engine_unavailable",
"service_unavailable", "maintenance", "idempotency_in_progress",
];
public async Task<Scan> ReadAsync(
byte[] imageBytes, string idempotencyKey, string? reference = null,
CancellationToken ct = default)
{
var body = new
{
image = Convert.ToBase64String(imageBytes),
reference,
options = new { return_portrait = false },
};
for (var attempt = 0; ; attempt++)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "scans")
{
Content = JsonContent.Create(body),
};
request.Headers.Add("Idempotency-Key", idempotencyKey);
using var response = await http.SendAsync(request, ct);
if (response.IsSuccessStatusCode)
return (await response.Content.ReadFromJsonAsync<Scan>(Api.Json, ct))!;
var envelope = await response.Content.ReadFromJsonAsync<ErrorEnvelope>(Api.Json, ct);
var error = envelope?.Error
?? new ApiError("unknown", response.ReasonPhrase ?? "", null, null);
if (!Retryable.Contains(error.Code) || attempt >= 3)
throw new ScanFailed((int)response.StatusCode, error);
var wait = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1 << attempt);
await Task.Delay(wait > TimeSpan.FromSeconds(60) ? TimeSpan.FromSeconds(60) : wait, ct);
}
}
}
Anahtar, metodun içindeki Guid.NewGuid() yerine kendi sisteminizden gelmeli – bir yükleme id'si ya da sipariş id'si gibi. Verinizden gelen bir anahtar süreç yeniden başlasa da aynı kalır; yeni üretilmiş bir GUID kalmaz.
Her hatanın şekli aynıdır: code, message, docs_url ve request_id. message değerine göre değil, her zaman code değerine göre dallanın ve desteğe yazarken request_id değerini loglayın. Tam liste hataları ele alma rehberinde. Erkenden karşılaşacağınız üç tanesi:
402 insufficient_credits– canlı bakiye tükendi. Yeniden denenemez.413 payload_too_large– gövde sınırı aşıyor. Fotoğrafı küçültün.401 unauthorized– anahtar eksik ya da kabul edilmedi. Yeniden denenemez.
IHttpClientFactory ile tipli bir istemci
Her istek için yeni bir HttpClient oluşturmayın. ASP.NET Core'da istemciyi bir kez kaydedin ve handler'ları factory'nin yönetmesine bırakın:
builder.Services.AddHttpClient<PassportClient>(http =>
{
http.BaseAddress = new Uri("https://api.doc.cheap/v1/");
http.Timeout = TimeSpan.FromSeconds(60);
http.DefaultRequestHeaders.Authorization = new("Bearer",
builder.Configuration["DocCheap:ApiKey"]);
});
Anahtarı yapılandırmada ya da bir gizli anahtar deposunda tutun, asla kaynak kodda değil. Canlı anahtar sk_live_ ile, sandbox anahtarı sk_sandbox_ ile başlar. Microsoft.Extensions.Http.Resilience paketinden bir resilience handler eklerseniz yeniden denemeyi yalnızca tek bir katmana bırakın; iki katman deneme sayısını katlar.
Daha az veri saklamak
Çağrıdan sonra görüntüyü kendiniz saklamayın. Sonuçtan ihtiyacınız olanı okuyun ve baytları atın. Yukarıdaki istemci zaten return_portrait: false gönderiyor, bu yüzden portre kırpıntısı yanıtta geri gelmez.
Canlı bir anahtarda sonuç, hesabınızın geçmiş süresi boyunca saklanır; varsayılan bir yıldır. Tek bir çağrı için hiçbir şey saklanmasın istiyorsanız options içinde retain_hours: 0 gönderin: hiçbir satır yazılmaz ve küçük resim oluşturulmaz. Bunun bedeli, o çağrının yeniden denemesinin yeniden oynatılamaması ve idempotency_replay_unavailable olarak dönmesidir. İkisini birden istiyorsanız sıfırın üzerinde kısa bir süre seçin. Pasaport tanıma rehberi tüm seçenekleri anlatıyor.
Canlıya geçiş
sk_sandbox_public yerine kendi sk_live_ anahtarınızı koyun; başka hiçbir şey değişmez. Kayıtlı anahtarlar dakikada 60 istekle sınırlıdır. Hâlâ bir sağlayıcı seçiyorsanız pasaport OCR API karşılaştırması, ücretli alternatifleri yayımladıkları fiyatlarla birlikte listeliyor.
Size bir soru: bugün MRZ'yi .NET'te kendiniz mi ayrıştırıyorsunuz, yoksa bu işi bir servise mi bırakıyorsunuz? Aşağıdaki yorumlarda bize anlatın.