StampMitraDevelopers

गाइड

Verification calls के लिए एरर, retry और idempotency कैसे सँभालें

सिर्फ़ 429 और 502 को jitter वाले backoff और कोशिशों की सख़्त सीमा के साथ retry कीजिए, और 400, 401, 402 या 403 को कभी नहीं। जोखिम timeout का है: StampMitra का reference कहता है कि verification calls idempotent नहीं हैं और referenceId idempotency key नहीं है, इसलिए attempt ledger बनाइए, अनजाने नतीजे के लिए request logs देखिए, और bank account verification को आँख मूँदकर retry मत कीजिए।

लेखक: StampMitra Developer Teamअंतिम अपडेट: यह पेज English में पढ़ें

एक फ़ाइनेंस टीम ने रात में छह हज़ार वेंडरों के GSTIN दोबारा जाँचने का job चलाया। सुबह देखा तो usage summary में वेंडरों की संख्या से कहीं ज़्यादा calls थीं, कई सौ calls दोहराई गई थीं, और कोई नहीं बता पा रहा था कि कौन-सा वेंडर जाँचा गया और कौन-सा नहीं। गड़बड़ किसी एक बड़े bug में नहीं थी। वह एक आम retry wrapper में थी जो timeout को 502 की तरह मानकर तुरंत दोबारा भेज देता था।

सीधा नियम यह है कि verification call को दोबारा भेजना दो हालात में सुरक्षित है और बाक़ी में जोखिम भरा। 429 को थोड़े इंतज़ार के बाद दोबारा भेजिए, क्योंकि request कोई काम होने से पहले ही लौटा दिया गया था। 502 को दो-तीन बार दोबारा भेजिए, क्योंकि नीचे की सेवा तक पहुँच नहीं हुई थी। बाक़ी हर हाल में काम की चाल दूसरी कोशिश नहीं, बल्कि फ़ैसला है: इनपुट सुधारिए, key सुधारिए, wallet भरिए, या किसी इंसान से पूछिए। सबसे ज़्यादा सावधानी उस मामले की चाहिए जिसे status codes बिल्कुल कवर नहीं करते: वह request जो timeout हुआ और आपको पता नहीं कि वह चला या नहीं।

यह मामला इसलिए मायने रखता है कि timeout के बाद आँख मूँदकर retry जाँच को दो बार चला सकता है, और आप दोनों का पैसा देते हैं। verification से मिला referenceId उसी call की पहचान है और साफ़ तौर पर idempotency key नहीं। सुरक्षा Idempotency-Key header से मिलती है, जिसे हर metered verification route मानता है: retry पर वही key भेजिए तो दूसरी जाँच के बजाय सहेजा हुआ response मिलता है। key के बिना retry नई call है। bank account verification के लिए, जिसमें बैंक के ज़रिए transfer शामिल है, दोहराव दूसरी fee से ज़्यादा भी कर सकता है।

यह गाइड verification के काम के बोझ के लिए retry की डिज़ाइन पर है। यह status code की बुनियादी बातें API एरर और उन्हें सँभालना पर छोड़ती है और उन हिस्सों पर ध्यान देती है जो तय करते हैं कि आपका integration भार में सुरक्षित रहेगा या नहीं: एक call के कौन-से नतीजे होते हैं, idempotency के लिए प्लेटफ़ॉर्म क्या देता है और क्या नहीं, अपना attempt ledger कैसे बनाएँ, अनजाने नतीजे को request logs से कैसे सुलझाएँ, backoff और rate-limit का नियंत्रण, queue की डिज़ाइन, और wallet ख़त्म होने पर क्या करें।

मुख्य बातें

  • हर call पाँच जगहों में से एक पर ख़त्म होती है: सफलता, नकारात्मक नतीजा जो खुद भी सफलता है, काम से पहले का इनकार, काम शुरू होने के बाद की विफलता, या अनजाना नतीजा। हर एक का retry नियम अलग है।
  • अपने-आप retry के उम्मीदवार सिर्फ़ 429 और 502 हैं, घातांकी backoff, jitter और कोशिशों की सख़्त सीमा के साथ। 400, 401, 402, 403 या 404 पर retry मत कीजिए।
  • valid: false का नतीजा पूरी हुई lookup है, एरर नहीं। production में वह बिल होता है और उसे आपके retry के रास्ते में नहीं जाना चाहिए।
  • हर verification call पर Idempotency-Key भेजिए और एक call के retries में वही key दोहराइए। referenceId call की पहचान है, idempotency key नहीं। अपना attempt ledger भी रखिए, उन हालात के लिए जिन्हें key नहीं ढकती (24 घंटे से पुरानी key, नई key के साथ दोबारा शुरू हुआ job)।
  • attempt ledger बनाइए: call से पहले इरादा और बाद में नतीजा दर्ज कीजिए, उस चीज़ के हिसाब से जिसे आप verify कर रहे हैं। हर call से पहले उसे देखिए।
  • अनजाने नतीजे को सुलझाने के लिए दोबारा भेजने से पहले request logs (GET /v1/logs) में पूरी हुई call ढूँढिए।
  • bank account verification वह endpoint है जहाँ आँख मूँदकर retry सबसे कम सुरक्षित है, क्योंकि उसमें बैंक transfer शामिल है।
  • concurrency को token bucket या semaphore से नियंत्रित कीजिए, अपने प्लान की सीमा से नीचे, और 429 को सिर्फ़ विफल request के लिए नहीं, सबके लिए धीमे होने का संकेत मानिए।
  • ख़ाली wallet (402) पर queue रोकिए और किसी इंसान को सूचित कीजिए। retry में घूमने से wallet नहीं भरता।
  • Idempotency-Key हर जगह एक ही नियम मानती है: वही key और वही request सहेजा हुआ response दोहराती है; वही key किसी अलग request के साथ 422 IDEMPOTENCY_KEY_REUSED देती है; पहली request चल ही रही हो तो 409 IDEMPOTENCY_IN_PROGRESS और Retry-After मिलता है। यह POST /v1/estamp/orders और POST /v1/batches पर ज़रूरी है (उसके बिना 400 IDEMPOTENCY_KEY_REQUIRED)।

रात का पूरा किस्सा: backfill में timeout, दो तरह से

ऊपर की टीम पर लौटिए। छह हज़ार वेंडर, रात का job, और एक retry wrapper। देखिए डिज़ाइन के बिना और डिज़ाइन के साथ वही रात कैसी बीतती है।

बिना ledger और गति-नियंत्रण के। job पचास समानांतर कनेक्शन खोलता है। दो-हज़ारवें वेंडर के आसपास नीचे की सेवा धीमी पड़ती है और timeout की लहर आती है। भोला retry wrapper timeout को 502 की तरह बरतकर तुरंत तीन बार दोबारा भेजता है। पचास workers अब उसी मिनट में दो सौ तक requests भेज सकते हैं। rate limit 429 से जवाब देती है, जिसे wrapper बिना backoff के तीन बार फिर भेजता है। queue पीछे जमा होती है, workers कनेक्शन थामे रहते हैं, और retries कई गुना हो जाते हैं। सुबह तक टीम कई सौ दोहराई lookups का पैसा दे चुकी है, usage summary वेंडरों की संख्या से कहीं ऊपर है, और कोई नहीं कह सकता कि कौन-से वेंडर verify हुए।

ऊपर की डिज़ाइन के साथ। token bucket कुल दर को प्लान की सीमा से नीचे रखता है, इसलिए 429 की लहर नहीं आती। नीचे की सेवा धीमी पड़े तो timeout UNKNOWN दर्ज होते हैं, retry नहीं होते। circuit breaker timeout की कड़ी के बाद खुल जाता है, workers रुकते हैं, फिर एक जाँच-call भेजते हैं, फिर चल पड़ते हैं। हर अनजाने attempt के लिए अगली बार logs में सही मिनट की पूरी हुई gstin.verify प्रविष्टि खोजी जाती है, और attempt या तो पूरा मान लिया जाता है या किसी इंसान के लिए चिह्नित होता है। 402 कभी नहीं आता क्योंकि कम-बैलेंस का अलर्ट पिछली दोपहर बज चुका था। सुबह मिलान दिखाता है कि प्लेटफ़ॉर्म और ledger की गिनती मुट्ठी भर calls के भीतर मिलती है, और क़रीब चालीस वेंडरों की सूची है जिन्हें दूसरी नज़र चाहिए।

फ़र्क़ चतुराई का नहीं था। चार सीधे नियंत्रणों का था: ledger, bucket, breaker और log की खोज।

एक call के पाँच नतीजे

हर call को पाँच जगहों में से एक पर ख़त्म होती मानिए। उन्हें नाम देने से retry का नियम लगभग यांत्रिक हो जाता है।

नतीजाकैसे दिखता हैक्या काम हुआ?production में बिल?क्या दोहराना सुरक्षित है?
सफलताHTTP 200, valid: trueहाँहाँज़रूरत नहीं
नकारात्मक सफलताHTTP 200, valid: false या found: falseहाँहाँ, यह पूरी हुई lookup हैतभी जब आपकी नीति बाद में ताज़ा जाँच चाहे
काम से पहले इनकार400, 401, 402, 403, 404, 429नहींनहींसिर्फ़ 429, रुककर। बाक़ी को सुधार चाहिए, retry नहीं
काम शुरू होने के बाद विफल502 INTERNAL_ERROR, बॉडी के साथ कनेक्शन टूटनाशायददर्ज है कि सफल जवाब के बाद ही बिलहाँ, सीमित बार
अनजानाTimeout, टूटा कनेक्शन, कोई जवाब नहींअनजानाअनजानानहीं, जाँचने तक

पाँचवीं पंक्ति वह जगह है जहाँ ज़्यादातर production घटनाएँ शुरू होती हैं। timeout विफलता नहीं है। वह जानकारी की ग़ैरमौजूदगी है, और जानकारी के अभाव का सही जवाब उसे जाकर खोजना है।

दूसरी पंक्ति वह जगह है जहाँ कई integration चुपचाप ग़लत बरतते हैं। नकारात्मक नतीजा 200 के रूप में आता है। जो कोड सिर्फ़ valid: true को सफलता मानता है, वह नकारात्मक नतीजे को एरर वाले हैंडलर में भेज सकता है, उन्हें retry कर सकता है, और हर retry का पैसा दे सकता है। नकारात्मक को सामान्य, बिल होने वाला, अंतिम जवाब मानकर सँभालिए।

बिलिंग से retry की सुरक्षा के बारे में क्या पता चलता है

reference कुछ बिलिंग नियम देता है जो डिज़ाइन को आकार देते हैं।

  • उपयोग सफल जवाब के बाद गिना जाता है। priced सेवाओं के लिए wallet तभी कटता है, पहले नहीं।
  • सकारात्मक और नकारात्मक, दोनों फ़ैसले पूरे हुए, बिल होने वाले verification हैं।
  • ग़लत बनावट का इनपुट किसी lookup से पहले ही 400 से विफल हो जाता है।
  • Sandbox की calls लॉग होती हैं पर wallet कभी नहीं काटतीं।

इनसे दो बातें निकलती हैं। पहली, काम से पहले लौटाया गया request (400, 401, 402, 403, 429) बिल नहीं होता, इसलिए उसे दोहराने में पैसा दाँव पर नहीं, बस एक बेकार call है। दूसरी, 502 या अनजाने नतीजे के लिए दस्तावेज़ कहता है कि बिल सफल जवाब के बाद लगता है, जिससे लगता है कि विफल call बिल नहीं होती, पर वह यह नहीं बताता कि जब जवाब लौटते समय खो जाए तब क्या होता है। इस किनारे पर अंदाज़े से कोई वित्तीय नियंत्रण मत बनाइए। प्लेटफ़ॉर्म ने क्या दर्ज किया यह logs से देखिए और मिलान के लिए usage summary इस्तेमाल कीजिए।

Idempotency: क्या मिलती है

Endpoint परिवारIdempotency-Key header
Verify endpoint (/v1/verify/*)वैकल्पिक, हर metered route पर मानी जाती है। referenceId key नहीं है
POST /v1/estamp/duty/calculate, POST /v1/estamp/orders/:orderId/partiesवैकल्पिक, मानी जाती है
POST /v1/orders, POST /v1/customers, POST /v1/payments/intentsवैकल्पिक, मानी जाती है
POST /v1/estamp/orders, POST /v1/batchesज़रूरी: बिना उसके 400 IDEMPOTENCY_KEY_REQUIRED
POST /v1/webhooks/events/:id/replayreplay उसी delivery को उसी payload के साथ उसी रास्ते से दोबारा चलाता है

जो भी route यह header लेता है उस पर नियम एक ही हैं:

स्थितिनतीजा
नई key के साथ पहली requestएक बार चलती है और उसका response key के नाम पर सहेजा जाता है
वही key, वही requestसहेजा हुआ response, वही status और वही body। न दूसरी call, न दूसरा शुल्क
वही key, अलग request422 IDEMPOTENCY_KEY_REUSED। कुछ नहीं चलता। अलग request के लिए नई key लीजिए
वही key, जबकि पहली request अभी चल रही है409 IDEMPOTENCY_IN_PROGRESS और Retry-After header। रुकिए, फिर ठीक वही request भेजिए
फ़ेल हुई पहली requestkey छोड़ दी जाती है, इसलिए वही key फिर आज़माई जा सकती है
key का दायराआपकी organisation, route और environment। sandbox की key live की key से कभी नहीं मिलती
रखने की अवधि24 घंटे। उसके बाद वही key नई request की तरह चलती है
key का रूप1 से 255 छपने योग्य अक्षर। बाक़ी सब 400 IDEMPOTENCY_KEY_INVALID

हर तार्किक काम के लिए एक key लीजिए, किसी स्थिर चीज़ से बनी जो आपकी है (payee id के साथ उद्देश्य, agreement id के साथ version), और उस काम के हर retry में वही रखिए। हर कोशिश पर random key उद्देश्य ही तोड़ देती है।

key नीचे वाले ledger की जगह नहीं लेती। वह 24 घंटे के भीतर के दोहराव को रोकती है; ledger उस job को भी ढकता है जो बिल्कुल नई key के साथ दोबारा शुरू होता है, और वहीं आप वह नतीजा दर्ज करते हैं जिसका मिलान बाद में करेंगे।

अपनी idempotency बनाना: attempt ledger

तरीक़ा सीधा है और ज़्यादातर काम कर देता है। call से पहले लिखिए कि आप करने जा रहे हैं। जवाब सुनने के बाद लिखिए कि क्या हुआ। किसी भी call से पहले ledger पढ़िए।

एक न्यूनतम तालिका:

sql
CREATE TABLE verification_attempts (
  id            BIGSERIAL PRIMARY KEY,
  subject_key   TEXT NOT NULL,         -- जो verify हो रहा है, जैसे 'gstin:07AAAAA1111A1Z1'
  purpose       TEXT NOT NULL,         -- 'ONBOARD', 'RECHECK', 'PAYMENT'
  window_start  TIMESTAMPTZ NOT NULL,  -- वह खाँचा जो एक खिड़की के भीतर दोहराव को एक ही attempt बनाता है
  state         TEXT NOT NULL,         -- 'STARTED', 'DONE', 'UNKNOWN', 'FAILED'
  reference_id  TEXT,
  outcome       JSONB,
  started_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  finished_at   TIMESTAMPTZ,
  UNIQUE (subject_key, purpose, window_start)
);

unique constraint ही idempotency है। दो workers जो एक ही उद्देश्य के लिए एक ही खिड़की में एक ही GSTIN verify करना चाहें, insert पर टकराते हैं, और सिर्फ़ एक आगे बढ़ता है। खिड़की (जैसे मौजूदा घंटा या दिन, इस पर निर्भर कि उस उद्देश्य के लिए आप कितनी बार ताज़ा नतीजा चाहते हैं) तय करती है कि "वही attempt" का मतलब क्या है। अगली तिमाही की दोबारा जाँच अलग खिड़की में है और अनुमत है।

call का wrapper:

Python
def verify_once(db, subject_key, purpose, window, do_call, find_in_logs):
    inserted = db.try_insert_attempt(subject_key, purpose, window, state="STARTED")
    if not inserted:
        prior = db.get_attempt(subject_key, purpose, window)
        if prior["state"] == "DONE":
            return prior["outcome"]                      # दोबारा उपयोग: दूसरी call नहीं, दूसरा शुल्क नहीं
        if prior["state"] in ("STARTED", "UNKNOWN"):
            found = find_in_logs(prior["started_at"])    # दोहराने से पहले सुलझाइए
            if found:
                db.finish_attempt(prior["id"], "DONE", found)
                return found
            raise RuntimeError("OUTCOME_UNRESOLVED")     # इंसान के लिए छोड़िए, फिर से मत भेजिए
    try:
        outcome = do_call()                              # एक HTTP request, भीतर सीमित 429/502 retries
        db.finish_attempt(inserted["id"], "DONE", outcome)
        return outcome
    except Exception as err:
        state = "UNKNOWN" if getattr(err, "kind", "") == "TIMEOUT" else "FAILED"   # timeout विफलता नहीं है
        db.finish_attempt(inserted["id"], state, {"error": getattr(err, "kind", None), "status": getattr(err, "status", None)})
        raise

दो बारीकियाँ ध्यान माँगती हैं। wrapper network call से पहले attempt लिखता है, इसलिए call और लिखने के बीच का crash भी STARTED पंक्ति छोड़ जाता है जिसे बाद की कोई run नोटिस करेगी। और वह timeout को FAILED नहीं, UNKNOWN में बदलता है, जिससे अगली run देखे बिना फिर से भेजने से बचती है।

अनजाने नतीजे को logs से सुलझाना

जब call timeout हुई, तो हो सकता है प्लेटफ़ॉर्म ने उसे चलाया हो या नहीं। request logs बता सकते हैं। GET /v1/logs आपके organisation की calls createdAt, service (जैसे pan.verify, gstin.verify या bank-account.verify) और status (success या failure) के साथ सूचीबद्ध करता है, और from, to, service, status, limit (200 तक) तथा cursor लेता है।

cURL
curl -G "https://api-b2b.stampmitra.in/v1/logs" \
  -H "Authorization: Bearer $STAMPMITRA_API_KEY" \
  --data-urlencode "service=bank-account.verify" \
  --data-urlencode "from=2026-10-29T09:40:00.000Z" \
  --data-urlencode "to=2026-10-29T09:45:00.000Z" \
  --data-urlencode "status=success"

खिड़की को अपनी कोशिश के आसपास के मिनट तक संकरा कीजिए और service से छानिए। सफल प्रविष्टि हो तो call चली थी, और उसे हो चुकी मानिए। log की प्रविष्टि समय और service देती है; वह मूल verification का जवाब वापस नहीं देती, इसलिए नतीजा आपके अपने ledger का है। यही वजह है कि नतीजा मिलते ही उसे सहेज लीजिए, और अनसुलझी calls को नीति चाहिए: अगर log सफलता दिखाए पर आपके सिस्टम ने जवाब खो दिया हो, तो डेटा वापस पाने के लिए दोबारा verify करना पड़ सकता है, और वह सोचा-समझा, दर्ज दूसरा call है, दुर्घटना नहीं।

Python
from datetime import datetime, timedelta, timezone
import requests

def find_in_logs(service, started_at, key):
    t = started_at if isinstance(started_at, datetime) else datetime.fromisoformat(started_at)
    params = {
        "service": service,
        "from": (t - timedelta(seconds=5)).astimezone(timezone.utc).isoformat().replace("+00:00", "Z"),
        "to": (t + timedelta(seconds=90)).astimezone(timezone.utc).isoformat().replace("+00:00", "Z"),
        "status": "success",
        "limit": "10",
    }
    r = requests.get("https://api-b2b.stampmitra.in/v1/logs", params=params,
                     headers={"Authorization": f"Bearer {key}"}, timeout=10)
    if not r.ok:
        return None
    items = r.json().get("items") or []
    return {"logged_at": items[0]["createdAt"], "source": "logs"} if items else None

मिलान समय और service से होता है, इसलिए अगर आप कई verification समानांतर चलाते हैं तो खिड़की में एक से ज़्यादा प्रविष्टियाँ हो सकती हैं। इसीलिए ledger हर विषय के हिसाब से है और log की खोज मुख्य कुंजी नहीं, फ़ैसला करने वाली चीज़ है। सबूत अस्पष्ट हो तो attempt को इंसान के लिए चिह्नित कीजिए और फिर से मत भेजिए।

status के हिसाब से retry की नीति

StatuscodeRetry?कैसेक्यों
400BAD_REQUESTनहींइनपुट सुधारिए, पहले ही validate कीजिएवही बॉडी उसी तरह फ़ेल होती है
401UNAUTHORIZEDनहींkey या header सुधारिए; अलर्टretry से key नहीं बनती
402PAYMENT_REQUIREDनहींqueue रोकिए; रिचार्ज; इंसान को अलर्टबैलेंस सिर्फ़ रिचार्ज से बदलता है
403FORBIDDENनहींscopes या allowlist सुधारिए; अलर्टअनुमतियाँ अपने-आप ठीक नहीं होतीं
404NOT_FOUNDनहींid या organisation सुधारिएपुरानी या पराई id ग़ायब ही रहती है
409IDEMPOTENCY_IN_PROGRESSहाँ, Retry-After के बादउसी key से ठीक वही request फिर भेजिएपहली request अभी चल रही है
409CONFLICTनहीं"पहले से मौजूद" मानिएअवस्था है, अस्थायी नहीं
422IDEMPOTENCY_KEY_REUSEDनहींनई request के लिए नई key लीजिएkey पहले ही किसी दूसरी request की है
429TOO_MANY_REQUESTSहाँjitter के साथ backoff; पूरे क्लाइंट को धीमा कीजिएखिड़की बीत जाती है
502INTERNAL_ERRORहाँ, कुछ बारjitter के साथ backoff; कोशिशें सीमितनीचे की सेवा सुधर सकती है
Timeout या टूटा कनेक्शनकोई नहींआँख मूँदकर नहींपहले सुलझाइए (ledger, logs), फिर शायद retrycall चली हो सकती है
बाक़ी 5xxअनजाना502 जैसा, सीमा के साथकच्चा status और कटी हुई बॉडी लॉग कीजिएदर्ज नहीं; code मत गढ़िए

हर 429 और 503, और 409 IDEMPOTENCY_IN_PROGRESS, सेकंडों में Retry-After header लेकर आता है। उसे न्यूनतम इंतज़ार मानिए; नीचे का backoff उसके ऊपर आपकी नीति है और तब लागू होता है जब header न हो।

429 पर एक अतिरिक्त विचार। वह हर endpoint की rate limit हो सकती है (दर्ज उदाहरण है किसी endpoint के लिए 60 सेकंड में 60 requests) या, sandbox key पर, sandbox सीमा (प्रति organisation प्रति मिनट 10 या प्रति दिन 200 requests, प्रति IP address प्रति दिन 50)। पहली और प्रति मिनट की sandbox सीमा एक मिनट में हट जाती हैं। दैनिक और IP सीमा आधी रात IST तक नहीं हटतीं। अगर sandbox key पर 429 कई मिनट बना रहे, तो retry रोक दीजिए, क्योंकि आज की requests शायद ख़त्म हैं; error code (SANDBOX_DAILY_LIMIT या SANDBOX_IP_LIMIT) और details.resetAt यही बताते हैं।

jitter के साथ backoff, कोड में

पूरे jitter वाला घातांकी backoff मानक रूप है: शून्य और घातांकी रूप से बढ़ती सीमा के बीच कोई बेतरतीब समय रुकिए। बेतरतीबी क्लाइंटों की भीड़ को एक साथ retry करने से रोकती है।

Python
import random, time

def with_retry(fn, retry_on=lambda e: getattr(e, "status", None) in (429, 502),
               max_attempts=4, base=0.5, cap=15.0, deadline=60.0):
    started = time.monotonic()
    attempt = 0
    while True:
        try:
            return fn()
        except Exception as err:
            attempt += 1
            if attempt >= max_attempts or not retry_on(err) or time.monotonic() - started > deadline:
                raise
            time.sleep(random.uniform(0, min(cap, base * 2 ** (attempt - 1))))

और JavaScript में वही:

JavaScript
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function withRetry(fn, { retryOn = (e) => e.status === 429 || e.status === 502, max = 4, base = 500, cap = 15_000, deadlineMs = 60_000 } = {}) {
  const started = Date.now();
  for (let attempt = 0; ; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt + 1 >= max || !retryOn(err) || Date.now() - started > deadlineMs) throw err;
      await sleep(Math.random() * Math.min(cap, base * 2 ** attempt)); // पूरा jitter
    }
  }
}

दोनों संस्करणों में तीन सीमाएँ जान-बूझकर हैं: कोशिशों की अधिकतम संख्या, किसी एक इंतज़ार पर ऊपरी सीमा, और कुल समय-सीमा। बिना समय-सीमा का retry लूप छोटे आउटेज को अटके workers के ढेर में बदल देता है। timeout retryOn से जान-बूझकर बाहर है, ऊपर बताए कारणों से; उसे सिर्फ़ ऐसे endpoint और call-प्रकारों के लिए जोड़िए जहाँ दोहराव हानिरहित है, और तभी जब ledger कहे कि कोई पहले की कोशिश सफल हो ही नहीं सकती थी।

अलग-अलग तरह की calls के लिए अलग नीतियाँ

सबके लिए एक retry नीति एक सुविधाजनक ग़लती है। सही अंक इस पर निर्भर हैं कि कौन इंतज़ार कर रहा है।

संदर्भकोशिशों की सीमाकुल समय-सीमासीमा पूरी होने पर
इंटरैक्टिव (यूज़र फ़ॉर्म देख रहा है)कम: दो कोशिशेंछोटी, एक call से कुछ सेकंड ऊपर"हम अभी जाँच रहे हैं" दिखाइए और बैकग्राउंड में जारी रखिए
बैकग्राउंड (worker आवेदन प्रोसेस करता है)मध्यम: चार कोशिशेंमिनटattempt चिह्नित कीजिए, बना रहे तो अलर्ट, पूरे job को बाद में फिर चलाइए
बड़ी मात्रा (backfill या दोबारा जाँच का sweep)हर वस्तु पर कम, कुल गति धीमीपूरी run के लिए घंटेविफलता दर्ज कीजिए, बाक़ी जारी रखिए, अंत में रिपोर्ट दीजिए

इंटरैक्टिव calls का लक्ष्य यूज़र को कभी चक्कर वाले चिह्न को घूरते न छोड़ना है, इसलिए काम सीमित कीजिए और बाक़ी बैकग्राउंड में भेजिए। बैकग्राउंड काम का लक्ष्य पूर्णता है, इसलिए बड़ा बजट ठीक है। बड़ी मात्रा के काम का लक्ष्य अच्छा पड़ोसी बनना है: हर वस्तु पर कम सीमा एक ख़राब रिकॉर्ड को पूरी run खाने से रोकती है, और धीमी गति आपके sweep को बाक़ी ट्रैफ़िक बिगाड़ने से रोकती है। तीनों में ledger साझा है, इसलिए अग्रभूमि में शुरू होकर बैकग्राउंड में ख़त्म होने वाला job उस call को कभी नहीं दोहराता जो पहले चल चुकी।

concurrency का नियंत्रण और rate limit

Backoff 429 के बाद उसे सँभालता है। Concurrency का नियंत्रण ज़्यादातर को होने से रोकता है। आपके प्लान की प्रति मिनट requests की एक सीमा है, और sandbox प्लान, pay-as-you-go तथा ऊपर के प्लान अलग हैं; दर्ज उदाहरण को कोड में बैठाने की जगह अपना अंक dashboard से पढ़िए।

सारे workers की साझा token bucket कुल दर को उस अंक के नीचे रखती है:

Python
import threading, time

class TokenBucket:
    def __init__(self, per_minute: int, burst: int = 5):
        self.capacity = burst
        self.tokens = float(burst)
        self.rate = per_minute / 60.0
        self.updated = time.monotonic()
        self.lock = threading.Lock()

    def take(self):
        while True:
            with self.lock:
                now = time.monotonic()
                self.tokens = min(self.capacity, self.tokens + (now - self.updated) * self.rate)
                self.updated = now
                if self.tokens >= 1:
                    self.tokens -= 1
                    return
                wait = (1 - self.tokens) / self.rate
            time.sleep(wait)

bucket = TokenBucket(per_minute=45)   # प्लान की सीमा से नीचे रखिए ताकि गुंजाइश बची रहे

दर को प्लान की सीमा से नीचे रखिए ताकि उसी key को बाँटती दूसरी सेवाओं और retries के लिए गुंजाइश बची रहे। जब 429 आए तो उसे पूरे क्लाइंट के बारे में जानकारी मानिए: bucket की दर कुछ देर के लिए घटाइए, सिर्फ़ विफल request का समय-क्रम नहीं। जो क्लाइंट एक request को पीछे हटाता है जबकि बाक़ी दस पूरी रफ़्तार से endpoint को पीट रहे हैं, वह पीछे नहीं हट रहा।

circuit breaker और bulkhead

जब नीचे की सेवा जूझ रही हो, तब आपका क्लाइंट सबसे दयालु काम यह कर सकता है कि कुछ देर पूछना बंद कर दे। circuit breaker हाल की विफलताओं (502 और timeout, 400 नहीं) की गिनती रखता है और सीमा पर खुल जाता है, ठंडा होने की अवधि तक calls को तुरंत विफल करता है, फिर एक अकेली जाँच-call जाने देता है। खुली अवस्था के दौरान आपके ऑनबोर्डिंग प्रवाह को काम को कतार में रखना चाहिए या "हम जल्द verify करेंगे" दिखाना चाहिए, timeout जमा नहीं करने चाहिए।

Bulkhead काम के बोझ को अलग रखते हैं ताकि एक दूसरे को भूखा न रखे। backfill जैसे बड़े jobs को अपना limiter दीजिए, इंटरैक्टिव limiter से नीचे, ताकि दोबारा जाँच का sweep कभी किसी यूज़र को इंतज़ार न करवाए। अगर आप एक key को कई सेवाओं में बाँटते हैं, तो साझा rate limit उन्हें जोड़ देती है; हर सेवा के लिए अलग key रखना, जैसा sandbox बनाम production API key में सुझाया है, logs में उनका उपयोग भी अलग करता है।

wallet ख़त्म होने पर: 402

संदेश Insufficient fulfilment wallet balance to cover this call. के साथ 402 PAYMENT_REQUIRED वह अकेला status है जिसे कोई कोड ठीक नहीं कर सकता। सिर्फ़ रिचार्ज उसे बदलता है। retry rate limit बर्बाद करता है, logs भरता है और असली समस्या छिपाता है।

इसे ऑपरेशन की अवस्था की तरह डिज़ाइन कीजिए, एरर की शाखा की तरह नहीं।

  1. पहले 402 पर queue को PAUSED_WALLET चिह्नित कीजिए, paid काम उठाना बंद कीजिए और नया काम queue में लेते रहिए।
  2. किसी इंसान को ऐसे संदेश से सूचित कीजिए जो बताए कि क्या रुका है और कितना इंतज़ार में है।
  3. यूज़रों को सच्चा संदेश दिखाइए ("हम अभी verification पूरा नहीं कर पा रहे"), verification की विफलता नहीं।
  4. wallet रिचार्ज हो तो queue को क्रम से फिर चलाइए, token bucket से दर सीमित रखते हुए।

कम बैलेंस जानने के लिए 402 का इंतज़ार मत कीजिए। dashboard में दिखने वाले usage summary और बैलेंस से अपना कम-बैलेंस का अलर्ट चलाइए, सामान्य खपत के दिनों में सीमा के साथ, ताकि 402 दुर्लभ हो। रिचार्ज पर GST लगता है और एक न्यूनतम रक़म है, और भुगतान के बाद GST इनवॉइस आता है, जो रिचार्ज की योजना बनाने की एक और वजह है, उन पर प्रतिक्रिया देने की नहीं।

wallet रिचार्ज होने के बाद queue को एक झटके में पूरा खोल देना भी ग़लती है। रुका हुआ सारा काम एक साथ चल पड़े तो token bucket की सीमा के बावजूद आपकी अपनी सेवा पर अचानक भार आ सकता है, और उसी समय किसी और टीम की calls भी उसी wallet से कट रही हों तो वह जल्दी फिर ख़ाली हो सकता है। queue को धीरे-धीरे खोलिए, पहले सबसे ज़रूरी काम (जैसे वह वेंडर जिसका भुगतान टला है) और फिर बाक़ी, और रिचार्ज के बाद के पहले कुछ मिनट बैलेंस की गिरावट देखते रहिए। अगर आपके पास staging और production के अलग wallet हैं, तो अलर्ट में यह साफ़ लिखिए कि कौन-सा ख़ाली हुआ, वरना रात के तीन बजे ग़लत wallet रिचार्ज करने वाले को खोजना पड़ता है।

मिलान: अपनी गिनती प्लेटफ़ॉर्म की गिनती से

retry की ग़लतियाँ उन अंकों में दिखती हैं जो मेल नहीं खाते। नियमित मिलान कीजिए, तो पहला डुप्लिकेट तभी दिख जाता है जब वह छोटा हो।

GET /v1/usage/summary मौजूदा बिलिंग अवधि के कुल लौटाता है: totalApiRequests, callsByService (जैसे pan.verify और estamp), callsByPeriod और callsByDay। हर service के callsByDay की तुलना अपने ledger की उसी दिन DONE अवस्था वाले attempts की गिनती से कीजिए।

अवलोकनसंभावित कारण
प्लेटफ़ॉर्म की गिनती आपकी DONE गिनती से ज़्यादाऐसी calls जो आपने कीं पर दर्ज नहीं कीं (call के बाद crash), ledger की पंक्ति के बिना चले retries, या उसी key को इस्तेमाल करती दूसरी सेवा
प्लेटफ़ॉर्म की गिनती आपके attempts से कमआपके attempts में काम से पहले लौटाई विफलताएँ हैं, जो बिल नहीं होतीं
गिनतियाँ कुछ प्रतिशत के भीतर मेल खाती हैंस्वस्थ। बचा हिस्सा आम तौर पर वे timeout हैं जो सचमुच चले
किसी एक दिन अचानक बदलावdeploy, queue का replay या backfill। उस दिन के logs देखिए

शुरू में रोज़ मिलान कीजिए। फ़र्क़ को dashboard पर लिखिए ताकि किसी को query चलाए बिना बहाव दिख जाए। प्लेटफ़ॉर्म और ledger के बीच बढ़ता फ़ासला बिना दर्ज retries का दस्तख़त है, और उसे इनवॉइस पढ़ने से गिनकर खोजना सस्ता है। usage summary और logs endpoint बाक़ी की तरह वही API key प्रमाणीकरण इस्तेमाल करते हैं, और logs का रिटेंशन आपके प्लान पर निर्भर है, इसलिए लंबे समय के रिकॉर्ड के रूप में अपना ledger रखिए।

queue: कम से कम एक बार की delivery और idempotent workers

बड़े पैमाने की ज़्यादातर verification queue से चलती है। queue संदेश कम से कम एक बार पहुँचाती हैं, यानी worker वही job दो बार देख सकता है, या तो इसलिए कि worker खत्म करके acknowledge करने से पहले crash हुआ, या इसलिए कि धीमी call के दौरान visibility timeout निकल गया। आपका worker idempotent होना चाहिए, और ऊपर वाला ledger उसे ऐसा बनाता है।

Python
def handle(job):
    key = f"gstin:{job['gstin']}"
    window = hour_bucket(job["enqueued_at"])   # वही job दोबारा पहुँचे तो उसी खिड़की में पड़ता है
    outcome = verify_once(db, subject_key=key, purpose="ONBOARD", window=window,
                          do_call=lambda: verify_gstin(job["gstin"]),
                          find_in_logs=lambda started: find_in_logs("gstin.verify", started, API_KEY))
    save_result(job["vendor_id"], outcome)

queue की तीन सेटिंग मायने रखती हैं। visibility timeout को अपनी सबसे लंबी call और retries से ज़्यादा रखिए, ताकि धीमी call दूसरे worker को न थमा दी जाए। जो jobs ग़ैर-retry-योग्य विफलता (400, 401, 403) पर गिरें, उन्हें एरर code और requestId जोड़कर dead-letter queue में भेजिए, ताकि कोई उन्हें देख सके। और दोबारा पहुँचाने की गिनती सीमित कीजिए, ताकि विषैला job हमेशा चक्कर न काटता रहे और हर चक्कर पर खुद का पैसा न लगवाए।

एक बात जो अक्सर छूट जाती है: queue के ऊपर एक "ज़हरीला संदेश" भी हो सकता है, यानी ऐसा job जो हर बार किसी अनजाने कारण से विफल होता है, जैसे ख़राब बनावट का इनपुट जो आपके ऑफ़लाइन जाँच से बच निकला। अगर worker उसे बार-बार उठाता रहे, तो वह बाक़ी काम की जगह घेरता है और हर चक्कर में कोई call भी ख़र्च हो सकती है। इसका सीधा इलाज यह है कि कोशिशों की सीमा पार होते ही job को अलग "मृत queue" में रख दीजिए, कारण के साथ, और किसी इंसान के लिए एक छोटी सूची बनाइए। वहाँ से उसे दोबारा चलाना हाथ का फ़ैसला रहे, अपने-आप का नहीं, और हर बार चलाने से पहले ledger देख लिया जाए कि उस विषय पर पहले से कोई call हो चुकी है या नहीं।

webhook प्राप्त करने वालों को भी यही अनुशासन चाहिए

verification के जवाब HTTP response में आते हैं, पर order और payment के events webhook के रूप में आते हैं, और deliveries दोहराई जा सकती हैं। प्लेटफ़ॉर्म X-Webhook-Signature, X-Webhook-Timestamp और X-Webhook-Event-Id headers भेजता है। signature आपके signing secret से timestamp, एक बिंदु और raw body का hexadecimal HMAC-SHA256 है। उसे raw body से verify कीजिए, फिर से serialise किए JSON से नहीं, और event id को अपनी डुप्लिकेट-पहचान की कुंजी बनाइए।

Python
import hmac, hashlib, time

def handle_webhook(headers, raw_body: bytes, secret: str, seen) -> int:
    ts = headers.get("X-Webhook-Timestamp", "")
    sig = headers.get("X-Webhook-Signature", "")
    event_id = headers.get("X-Webhook-Event-Id", "")
    expected = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    try:
        fresh = abs(time.time() - int(ts)) <= 300
    except ValueError:
        fresh = False
    if not (sig and fresh and hmac.compare_digest(expected, sig)):
        return 401
    if seen.has(event_id):
        return 200                      # डुप्लिकेट delivery: स्वीकार कीजिए और कुछ मत कीजिए
    seen.add(event_id)                  # काम से पहले, या उसी transaction में दर्ज कीजिए
    process_event(raw_body)
    return 200

300 सेकंड की खिड़की SDK का डिफ़ॉल्ट और समझदार replay-रक्षा है; प्लेटफ़ॉर्म खुद कोई सहनशीलता तय नहीं करता। event id को उसी transaction में दर्ज कीजिए जिसमें उससे ट्रिगर हुआ काम है, ताकि crash के बाद काम हो चुका हो और id दर्ज न हो, ऐसा न हो। events छूट जाएँ तो GET /v1/webhooks/events delivery attempts सूचीबद्ध करता है और POST /v1/webhooks/events/:id/replay एक को दोबारा चलाता है, इसलिए आपके हैंडलर को वही event दोबारा आने पर भी चलना चाहिए। और भाषाओं के verification snippets webhooks पेज पर हैं।

खोज के तीन नतीजे हो सकते हैं और हर एक का अपना सही बरताव है। अगर सफल प्रविष्टि मिली, तो ledger में उस विषय को "हो चुकी" लिखिए। चूँकि log मूल जवाब वापस नहीं देता, अगर आपको जवाब का डेटा चाहिए तो एक नई call करनी पड़ेगी, और वह अलग से बिल होगी। इसलिए पहले पूछिए कि क्या आपको सच में उस डेटा की ज़रूरत है, या सिर्फ़ यह जानना काफ़ी था कि जाँच हुई। अगर कोई प्रविष्टि नहीं मिली, तो इसे तुरंत "call नहीं चली" मत मानिए। प्रविष्टि के दिखने में कुछ देर लग सकती है, और reference इस देरी का कोई आँकड़ा नहीं बताता, इसलिए थोड़ी देर बाद खिड़की एक बार और खोजिए और तभी आगे बढ़िए। अगर failure वाली प्रविष्टि मिली, तो उसे सफल नतीजे की तरह मत गिनिए और ledger में वह विफलता दर्ज कीजिए जो असल में हुई।

timeout चुनना

timeout अपने यूज़र अनुभव से चुनिए, इस गाइड के किसी अंक से नहीं, क्योंकि reference जवाब के समय के आँकड़े प्रकाशित नहीं करता। कुछ सिद्धांत।

  • connect timeout को कुल timeout से छोटा रखिए, ताकि मरा हुआ रास्ता जल्दी विफल हो।
  • हर call के लिए संदर्भ के हिसाब से कुल timeout रखिए: इंटरैक्टिव फ़ॉर्म के लिए छोटा, बैकग्राउंड job के लिए लंबा।
  • कुल timeout को queue के visibility timeout और अपनी सेवा के आगे के किसी भी gateway timeout से नीचे रखिए।
  • एक हफ़्ते अपने नेटवर्क पर हर endpoint की असली देरी नापिए, फिर timeout धीमी पूँछ और गुंजाइश के पास रखिए।
  • timeout को अनजाना मानिए और उसे retry के रास्ते की जगह ledger के रास्ते भेजिए।

बहुत छोटा timeout उन calls के लिए अनजाने नतीजे बनाता है जो सफल होने ही वाली थीं। बहुत लंबा timeout आउटेज के दौरान workers को बाँध लेता है। production ट्रैफ़िक के पहले महीने के बाद अंक की समीक्षा कीजिए।

हर call के लिए क्या लॉग करें

retry का तर्क तभी डीबग हो सकता है जब हर कोशिश निशान छोड़े। हर कोशिश के लिए लॉग कीजिए: विषय की कुंजी (छिपाकर), endpoint, कोशिश की संख्या, HTTP status, विफलता पर error.code और requestId, सफलता पर referenceId, लगा समय और लिया गया फ़ैसला (स्वीकार, N ms में retry, रुकना)। API key या पूरा निजी डेटा कभी लॉग मत कीजिए। क्या रखें और कितने समय, यह verification कॉल से क्या लॉग करें में है।

अपने retry के बरताव को परखना

इसका ज़्यादातर बिना पैसा ख़र्च किए परखा जा सकता है।

  • नक़ली क्लाइंट के साथ unit tests। 429, 429, फिर सफलता लौटाइए और दो इंतज़ार तथा एक नतीजे पर assert कीजिए। 400 लौटाइए और assert कीजिए कि retry नहीं हुआ। timeout लौटाइए और assert कीजिए कि attempt UNKNOWN है।
  • Ledger के tests। एक ही विषय पर दो workers चलाइए और assert कीजिए कि एक call हुई। call और लिखने के बीच crash का अनुकरण कीजिए और assert कीजिए कि अगली run logs देखती है।
  • असली HTTP रास्ते के लिए sandbox। दर्ज sandbox वैल्यू माँगने पर valid और invalid नतीजा देती हैं, और ग़लत बनावट वाली वैल्यू 400। मनगढ़ंत key 401 देती है और scope के बिना key 403। sandbox wallet कभी नहीं काटता, इसलिए वह 402 नहीं दे सकता, और माँगने पर 502 भी पैदा नहीं कर सकता; उन बॉडी को लौटाने वाला लोकल सर्वर इस्तेमाल कीजिए। sandbox क्या साबित करता है और क्या नहीं, यह sandbox टेस्टिंग गाइड में है।
  • एक chaos run। staging क्लाइंट को ऐसे proxy की ओर मोड़िए जो forward करने के बाद दस में से एक जवाब गिरा दे, और पक्का कीजिए कि आपका ledger और logs की खोज डुप्लिकेट की गिनती शून्य रखते हैं।

किसी retry-घटना के बाद की समीक्षा में पाँच सवाल काफ़ी हैं। पहला, कौन-सी call अनजाने नतीजे पर ख़त्म हुई और ledger ने उसे कैसे दर्ज किया। दूसरा, क्या किसी विषय पर दो बार call गई, और अगर गई तो ledger की जाँच क्यों नहीं रुकी। तीसरा, क्या कोई ऐसा status retry हुआ जो होना ही नहीं चाहिए था (400, 401, 402, 403 या 404)। चौथा, मिलान में फ़र्क़ कितनी जल्दी दिखा, और अगर देर से दिखा तो अलर्ट कहाँ चूका। पाँचवाँ, ग्राहक या वेंडर ने क्या देखा, और क्या उसे सच्चा संदेश मिला। इन जवाबों को घटना-रिपोर्ट में लिखिए और जो नियम बदलना पड़े उसे ऊपर के परीक्षणों में एक नए मामले के रूप में जोड़िए, ताकि वही घटना दोबारा चुपचाप न लौटे।

आम ग़लतियाँ

  • valid: false को एरर मानकर उसे retry करना।
  • पहली call चली या नहीं देखे बिना timeout को retry करना।
  • 400, 401, 402 या 403 पर लूप में retry करना।
  • referenceId को idempotency key की तरह इस्तेमाल करना।
  • इरादे का कोई रिकॉर्ड न रखना, ताकि crash उस call का कोई निशान न छोड़े जो शायद बिल हुई हो।
  • एक request को पीछे हटाना जबकि क्लाइंट का बाक़ी हिस्सा पूरी रफ़्तार से भेजता रहे।
  • कोशिशों की अधिकतम संख्या या समय-सीमा के बिना retry करना।
  • 402 पर queue रोककर इंसान को सूचित करने की जगह उसी पर घूमते रहना।
  • queue का visibility timeout call से छोटा रखना, ताकि दो workers एक job प्रोसेस करें।
  • हर retry पर नई Idempotency-Key बनाना, जिससे retry नई call बन जाती है।
  • 422 IDEMPOTENCY_KEY_REUSED को उसी key और body से दोबारा भेजना। वह उसी तरह फ़ेल होगा; key पहले से किसी दूसरी request की है।

आगे क्या करें

पहले attempt ledger जोड़िए, क्योंकि वह आपको सबसे महँगी ग़लती से बचाता है। फिर अपनी calls को ऊपर के retry function में लपेटिए, status की तालिका को उसकी नीति बनाकर, सामने token bucket रखिए, और अपने workers को idempotent बनाइए। status codes के लिए API एरर और उन्हें सँभालना पढ़िए। यह पूरे प्रवाह में कहाँ बैठता है, यह वेंडर ऑनबोर्डिंग फ़्लो में है। हर endpoint क्या लौटाता है, यह verification API reference में है, और quickstart से sandbox key मिलती है। pay-as-you-go पर PAN की एक lookup ₹5 प्रति call है, इसलिए डुप्लिकेट असली लागत है; pricing पेज देखिए। सवाल support को भेजिए।

API reference एक नज़र में

PAN

PAN: paramsPOST /v1/verify/pan

Request parameters

Request parameters
FieldTypeDescription
AuthorizationrequiredheaderBearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted.
panrequiredstringPAN, format ABCDE1234F.
namestringName to match against records (max 256 chars).
PAN: responsePOST /v1/verify/pan
{
  "referenceId": "SMV-20260927-8F42K7",
  "pan": "ABCDE1234F",
  "type": "Individual",
  "valid": true,
  "registeredName": "RAHUL KUMAR",
  "fatherName": "SURESH KUMAR",
  "panStatus": "VALID",
  "panStatusCode": "E",
  "panStatusDescription": "Existing and valid PAN",
  "nameMatchResult": "MATCH",
  "nameMatchScore": "100",
  "aadhaarSeedingStatus": "y",
  "aadhaarSeedingStatusDescription": "Aadhaar linked",
  "lastUpdatedAt": "2023-04-01",
  "message": "PAN is Valid"
}
PAN: pricePOST /v1/verify/pan

PAN: ₹5 per successful call, pay-as-you-go (lower on Growth and Scale).

अक्सर पूछे जाने वाले सवाल

StampMitra के कौन-से एरर अपने-आप retry करने चाहिए?

सिर्फ़ 429 TOO_MANY_REQUESTS रुककर और 502 INTERNAL_ERROR कुछ बार, घातांकी backoff, jitter और कोशिशों तथा कुल समय की सख़्त सीमा के साथ। 400, 401, 402, 403 या 404 पर retry मत कीजिए, क्योंकि वही request उसी तरह फ़ेल होता है।

क्या verification calls idempotent हैं?

दर्ज रूप में नहीं। reference कहता है कि referenceId एक call की पहचान है और idempotency key नहीं है, और verify endpoint के लिए कोई idempotency header दर्ज नहीं। दोहराव दूसरे referenceId के साथ दूसरी call है और production में शायद दूसरा शुल्क।

verification call timeout हो जाए तो क्या करें?

उसे विफल नहीं, अनजाना मानिए। call से पहले attempt दर्ज कीजिए, timeout पर उसे UNKNOWN कीजिए, और दोहराने से पहले GET /v1/logs में उस service की सही मिनट की पूरी हुई call ढूँढिए। bank account verification को आँख मूँदकर retry करना सबसे कम सुरक्षित है।

क्या Idempotency-Key header verification के लिए चलता है?

नहीं। वह सिर्फ़ POST /v1/orders और POST /v1/payments/intents पर वैकल्पिक header के रूप में दर्ज है, वैकल्पिक idempotency key कहकर। key कितनी देर याद रखी जाती है और डुप्लिकेट पर क्या लौटता है, यह निर्दिष्ट नहीं, इसलिए sandbox में परखिए और support से पक्का कीजिए।

क्या valid false एक एरर है जिसे retry करना चाहिए?

नहीं। valid false या found false का नतीजा HTTP 200 में आता है और पूरी हुई lookup है जो production में बिल होती है। इसे एरर हैंडलर या retry लूप में नहीं, अपने सामान्य नतीजे के रास्ते पर अंतिम जवाब की तरह सँभालिए।

verification के लिए अपनी idempotency कैसे बनाएँ?

विषय, उद्देश्य और समय की खिड़की से बना attempt ledger रखिए, unique constraint के साथ। call से पहले STARTED पंक्ति डालिए, बाद में नतीजे के साथ उसे ख़त्म कीजिए, और दोबारा call करने की जगह DONE पंक्ति का उपयोग कीजिए। timeout को UNKNOWN कीजिए ताकि अगली run पहले logs देखे।

402 insufficient wallet balance को कैसे सँभालें?

paid queue रोकिए, किसी इंसान को अलर्ट कीजिए और यूज़रों को verification की विफलता की जगह सच्चा संदेश दिखाइए। 402 सिर्फ़ रिचार्ज से ठीक होता है, इसलिए retry rate limit बर्बाद करता है। रिचार्ज के बाद क्रम से फिर चलाइए, और अपना कम-बैलेंस अलर्ट रखिए ताकि 402 दुर्लभ हो।

rate limit तक पहुँचने से कैसे बचें?

workers में साझा token bucket रखिए और उसकी दर अपने प्लान की सीमा से नीचे रखिए, जो आपको dashboard से पढ़नी चाहिए। 429 आए तो सिर्फ़ विफल request को नहीं, पूरे क्लाइंट को कुछ देर धीमा कीजिए, और retry में jitter वाला backoff जोड़िए।

queue workers को दोहराई delivery से कैसे सुरक्षित करें?

queue कम से कम एक बार पहुँचाती है, इसलिए worker को attempt ledger से idempotent बनाइए। visibility timeout को सबसे लंबी call और retries से ऊपर रखिए, ग़ैर-retry-योग्य विफलताएँ requestId के साथ dead-letter queue में भेजिए, और दोबारा पहुँचाने की गिनती सीमित कीजिए।

webhook deliveries की डुप्लिकेट कैसे पहचानें?

timestamp, एक बिंदु और raw body के HMAC-SHA256 signature को verify कीजिए, फिर X-Webhook-Event-Id header को डुप्लिकेट-पहचान की कुंजी बनाइए, जो काम के साथ उसी transaction में दर्ज हो। events endpoint से replay की गई deliveries का payload वही रहता है, इसलिए हैंडलर को दोहराव सहना चाहिए।

स्रोत

  • StampMitra API reference डेटा: एरर का आकार और codes, यह नोट कि referenceId idempotency key नहीं है, Idempotency-Key header और उसके नियम, और logs तथा webhook events endpoint, जैसा इस रिपॉज़िटरी में 2026-10-29 को है।
  • StampMitra का webhook signing विवरण (headers X-Webhook-Signature, X-Webhook-Timestamp, X-Webhook-Event-Id और HMAC-SHA256 योजना), जैसा इस रिपॉज़िटरी में 2026-10-29 को है।
  • StampMitra sandbox का बरताव, sandbox responder से लिया गया, 2026-10-29।
  • jitter के साथ retry और idempotent consumers पर सामान्य इंजीनियरिंग अभ्यास, जैसा व्यापक रूप से इस्तेमाल होने वाले queueing और API प्लेटफ़ॉर्मों के सार्वजनिक दस्तावेज़ों में बताया गया है (2026)। किसी प्लेटफ़ॉर्म-विशेष का अंक उद्धृत नहीं किया गया।