आपके पास पासपोर्ट की एक फ़ोटो है और एक .NET 8 सर्विस है, जिसे नाम, नंबर, तारीखें और मशीन-रीडेबल ज़ोन (MRZ) डेटा के रूप में चाहिए। यह पोस्ट वही क्लाइंट सादे C# 12 में बनाती है: HttpClient, System.Net.Http.Json और System.Text.Json, बिना किसी SDK के। इसमें पहली कॉल, जवाब के लिए records, वे स्टेटस जो एरर नहीं हैं, ऐसे रीट्राई जो कभी दो बार चार्ज नहीं कर सकते, कोड के आधार पर एरर हैंडलिंग और ASP.NET Core के लिए एक टाइप्ड क्लाइंट शामिल हैं।

API है doc.cheap का पासपोर्ट और ID कार्ड OCR API। यह सिंक्रोनस है: एक POST, एक जवाब, पोल करने के लिए कोई job id नहीं।

पहली कॉल, बिना अकाउंट के

पब्लिक sandbox key है sk_sandbox_public। इसके लिए साइनअप की ज़रूरत नहीं और यह कोई क्रेडिट नहीं काटती। इस पर हर IP एड्रेस से एक घंटे में 10 रिक्वेस्ट और कुल मिलाकर कुछ ही मुफ़्त कोशिशों की सीमा है, जिसके बाद यह 403 registration_required लौटाती है।

रिक्वेस्ट बॉडी JSON है, जिसमें एक ही ज़रूरी फ़ील्ड है, image: फ़ाइल के बाइट्स base64 में। JPEG और PNG स्वीकार होते हैं; बाकी सब 415 unsupported_media_type के साथ लौटा दिया जाता है, और यह जाँच फ़ाइल के नाम से नहीं, उसके सिग्नेचर से होती है।

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());

रिक्वेस्ट स्कीमा सख़्त है। बॉडी में कोई अनजान key हो तो 422 validation_failed मिलता है, इसलिए किसी ऑप्शन के नाम में टाइपो चुपचाप नज़रअंदाज़ नहीं होता, साफ़ एरर देता है।

जवाब में क्या आता है

हर जवाब में वही आठ ग्रुप होते हैं, और हर key हमेशा मौजूद रहती है: meta, document, holder, fields, mrz, images, quality और authenticity। एक छोटा किया गया, मास्क किया हुआ उदाहरण:

{
  "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>" }
}

जब कुछ भी पढ़ा नहीं गया, तो document और holder की वैल्यू null होती है। mrz.status की वैल्यू passed, failed या absent होती है, और passed का मतलब है कि चेक डिजिट मेल खाते हैं।

जवाब के लिए records

JSON snake_case में है। .NET 8 में JsonNamingPolicy.SnakeCaseLower पहले से मौजूद है, इसलिए records सामान्य C# नाम रख सकते हैं। अनजान प्रॉपर्टी डिफ़ॉल्ट रूप से नज़रअंदाज़ होती हैं, इसलिए आपको सिर्फ़ वही घोषित करना है जो आप इस्तेमाल करते हैं।

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);

यहाँ तारीखें जानबूझकर स्ट्रिंग ही रखी गई हैं। जहाँ ज़रूरत हो, उन्हें DateOnly.TryParse से पार्स करें, ताकि किसी दस्तावेज़ पर एक अजीब वैल्यू डीसीरियलाइज़र के अंदर एक्सेप्शन न फेंके।

धुंधली फ़ोटो कोई एक्सेप्शन नहीं है

200 का मतलब हमेशा यह नहीं होता कि दस्तावेज़ पढ़ लिया गया। meta.status पर ब्रांच करें:

meta.status मतलब क्या करें
recognized दस्तावेज़ का प्रकार तय हुआ और उससे डेटा निकला document, holder, mrz इस्तेमाल करें
no_document_found फ़्रेम में दस्तावेज़ जैसा कुछ नहीं था नई फ़ोटो माँगें
unreadable दस्तावेज़ था, पर काम का कोई टेक्स्ट नहीं पढ़ा गया ज़्यादा साफ़ फ़ोटो माँगें
unsupported_document दस्तावेज़ मिला, पर उसका प्रकार जाना-पहचाना नहीं है यूज़र को बताएँ
rejected सर्विस की तरफ़ रिकग्निशन फ़ेल हुआ meta.id लॉग करें और बाद में फिर कोशिश करें

पैसा कौन देता है: billed फ़्लैग

sandbox key कभी कोई क्रेडिट नहीं काटती। लाइव key पर स्कैन तभी चार्ज होता है जब वह बिल होने लायक हो: दस्तावेज़ का प्रकार तय हुआ, और इनमें से कम से कम एक पढ़ा गया – ऐसा MRZ जिसके चेक डिजिट पास हों, प्रिंटेड ज़ोन के पाँच या ज़्यादा फ़ील्ड, या डिकोड हुआ बारकोड। वरना रिज़र्व किया गया क्रेडिट छोड़ दिया जाता है। sandbox key पर भी meta.billed बताता है कि स्कैन इस नियम पर खरा उतरा या नहीं, लेकिन कोई बैलेंस नहीं छुआ जाता।

meta.billed बताता है कि मामला किस तरफ़ गया। इसे अपने ऑर्डर या यूज़र के साथ सेव करें, फिर आपको अंदाज़े से चार्ज का मिलान कभी नहीं करना पड़ेगा। लाइव अकाउंट को हर महीने 100 मुफ़्त रिकग्निशन भी मिलते हैं।

ऐसे रीट्राई जो दो बार चार्ज नहीं कर सकते

आपकी तरफ़ टाइमआउट होने का मतलब यह नहीं कि हमारी तरफ़ स्कैन नहीं चला। Idempotency-Key हेडर (1 से 255 कैरेक्टर) भेजें और उसी दस्तावेज़ के हर रीट्राई में उसे दोबारा इस्तेमाल करें। लाइव key पर सर्वर key को बॉडी के फ़िंगरप्रिंट के साथ मिलाता है:

  • वही key, वही बॉडी: पहला नतीजा दोबारा भेजा जाता है, और दूसरा क्रेडिट नहीं कटता।
  • वही key, अलग बॉडी: 409 idempotency_conflict। नई key इस्तेमाल करें।
  • पहली कॉल अभी चल रही है: दूसरी कॉल थोड़ा इंतज़ार करती है, फिर उसे 409 idempotency_in_progress मिलता है। key वही रखें और रीट्राई करें।
  • पहला नतीजा अब सेव नहीं है: 409 idempotency_replay_unavailable। नई key इस्तेमाल करें।

अगर क्रेडिट रिज़र्व होने के बाद रिकग्निशन इंजन फ़ेल हो जाए, तो रिज़र्वेशन छोड़ दिया जाता है और key एक सही रीट्राई के लिए खाली हो जाती है। पूरी जानकारी idempotency के साथ सुरक्षित रीट्राई की गाइड में है।

पूरा क्लाइंट

यही वर्ज़न रखने लायक है। यह सिर्फ़ उन्हीं कोड पर रीट्राई करता है जिन पर रीट्राई का फ़ायदा है, 60 सेकंड तक Retry-After का पालन करता है, सभी कोशिशों में एक ही key रखता है और बाकी हर एरर को ऐसे एक्सेप्शन में बदल देता है जिसमें कोड मौजूद हो।

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);
        }
    }
}

key आपके अपने सिस्टम से आनी चाहिए – जैसे अपलोड id या ऑर्डर id – न कि मेथड के अंदर Guid.NewGuid() से। आपके डेटा से बनी key प्रोसेस के रीस्टार्ट के बाद भी बची रहती है; नया बना GUID नहीं।

हर एरर का आकार एक जैसा है: code, message, docs_url और request_id। ब्रांचिंग code पर करें, message पर कभी नहीं, और सपोर्ट को लिखते समय request_id लॉग में रखें। पूरी सूची एरर संभालने की गाइड में है। तीन जिनसे आपका सामना जल्दी होगा:

  • 402 insufficient_credits – लाइव बैलेंस ख़त्म हो गया। रीट्राई करने लायक नहीं।
  • 413 payload_too_large – बॉडी सीमा से बड़ी है। फ़ोटो का साइज़ घटाएँ।
  • 401 unauthorized – key नहीं है या स्वीकार नहीं हुई। रीट्राई करने लायक नहीं।

IHttpClientFactory के साथ टाइप्ड क्लाइंट

हर रिक्वेस्ट के लिए नया HttpClient न बनाएँ। ASP.NET Core में क्लाइंट को एक बार रजिस्टर करें और हैंडलर संभालने का काम फ़ैक्टरी पर छोड़ दें:

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"]);
});

key को कॉन्फ़िगरेशन या किसी सीक्रेट स्टोर में रखें, सोर्स कोड में कभी नहीं। लाइव key sk_live_ से शुरू होती है, sandbox key sk_sandbox_ से। अगर आप Microsoft.Extensions.Http.Resilience से कोई रेज़िलिएंस हैंडलर जोड़ते हैं, तो रीट्राई सिर्फ़ एक लेयर को करने दें; दो लेयर कोशिशों की संख्या को गुणा कर देती हैं।

कम डेटा रखना

कॉल के बाद इमेज को ख़ुद सेव न करें। नतीजे से जो चाहिए वह पढ़ें और बाइट्स हटा दें। ऊपर का क्लाइंट पहले से return_portrait: false भेजता है, इसलिए पोर्ट्रेट का क्रॉप जवाब में वापस नहीं आता।

लाइव key पर नतीजा आपके अकाउंट की हिस्ट्री अवधि तक रखा जाता है, जो डिफ़ॉल्ट रूप से एक साल है। किसी एक कॉल के लिए कुछ भी न रखना हो, तो options में retain_hours: 0 भेजें: न कोई पंक्ति लिखी जाती है, न कोई थंबनेल बनता है। इसकी क़ीमत यह है कि उस कॉल का रीट्राई नतीजा दोबारा नहीं दे सकता और idempotency_replay_unavailable लौटाता है। दोनों चाहिए, तो शून्य से ऊपर कोई छोटी अवधि चुनें। हर ऑप्शन पासपोर्ट रिकग्निशन गाइड में है।

लाइव पर जाना

sk_sandbox_public की जगह अपनी sk_live_ key लगाएँ, और कुछ और नहीं बदलता। रजिस्टर्ड key एक मिनट में 60 रिक्वेस्ट तक सीमित है। अगर आप अभी प्रोवाइडर चुन रहे हैं, तो पासपोर्ट OCR API की तुलना में पेड विकल्प उनकी प्रकाशित क़ीमतों के साथ दिए गए हैं।

आपसे एक सवाल: क्या आज आप .NET में MRZ ख़ुद पार्स करते हैं, या इसके लिए किसी सर्विस पर भरोसा करते हैं? नीचे टिप्पणियों में हमें बताएँ।