एक फ़ाइनेंस टीम ने रात में छह हज़ार वेंडरों के 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 दोहराइए।referenceIdcall की पहचान है, 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 के साथ 422IDEMPOTENCY_KEY_REUSEDदेती है; पहली request चल ही रही हो तो 409IDEMPOTENCY_IN_PROGRESSऔरRetry-Afterमिलता है। यहPOST /v1/estamp/ordersऔरPOST /v1/batchesपर ज़रूरी है (उसके बिना 400IDEMPOTENCY_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/replay | replay उसी delivery को उसी payload के साथ उसी रास्ते से दोबारा चलाता है |
जो भी route यह header लेता है उस पर नियम एक ही हैं:
| स्थिति | नतीजा |
|---|---|
| नई key के साथ पहली request | एक बार चलती है और उसका response key के नाम पर सहेजा जाता है |
| वही key, वही request | सहेजा हुआ response, वही status और वही body। न दूसरी call, न दूसरा शुल्क |
| वही key, अलग request | 422 IDEMPOTENCY_KEY_REUSED। कुछ नहीं चलता। अलग request के लिए नई key लीजिए |
| वही key, जबकि पहली request अभी चल रही है | 409 IDEMPOTENCY_IN_PROGRESS और Retry-After header। रुकिए, फिर ठीक वही request भेजिए |
| फ़ेल हुई पहली request | key छोड़ दी जाती है, इसलिए वही 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 पढ़िए।
एक न्यूनतम तालिका:
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:
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 -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 है, दुर्घटना नहीं।
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 की नीति
| Status | code | Retry? | कैसे | क्यों |
|---|---|---|---|---|
| 400 | BAD_REQUEST | नहीं | इनपुट सुधारिए, पहले ही validate कीजिए | वही बॉडी उसी तरह फ़ेल होती है |
| 401 | UNAUTHORIZED | नहीं | key या header सुधारिए; अलर्ट | retry से key नहीं बनती |
| 402 | PAYMENT_REQUIRED | नहीं | queue रोकिए; रिचार्ज; इंसान को अलर्ट | बैलेंस सिर्फ़ रिचार्ज से बदलता है |
| 403 | FORBIDDEN | नहीं | scopes या allowlist सुधारिए; अलर्ट | अनुमतियाँ अपने-आप ठीक नहीं होतीं |
| 404 | NOT_FOUND | नहीं | id या organisation सुधारिए | पुरानी या पराई id ग़ायब ही रहती है |
| 409 | IDEMPOTENCY_IN_PROGRESS | हाँ, Retry-After के बाद | उसी key से ठीक वही request फिर भेजिए | पहली request अभी चल रही है |
| 409 | CONFLICT | नहीं | "पहले से मौजूद" मानिए | अवस्था है, अस्थायी नहीं |
| 422 | IDEMPOTENCY_KEY_REUSED | नहीं | नई request के लिए नई key लीजिए | key पहले ही किसी दूसरी request की है |
| 429 | TOO_MANY_REQUESTS | हाँ | jitter के साथ backoff; पूरे क्लाइंट को धीमा कीजिए | खिड़की बीत जाती है |
| 502 | INTERNAL_ERROR | हाँ, कुछ बार | jitter के साथ backoff; कोशिशें सीमित | नीचे की सेवा सुधर सकती है |
| Timeout या टूटा कनेक्शन | कोई नहीं | आँख मूँदकर नहीं | पहले सुलझाइए (ledger, logs), फिर शायद retry | call चली हो सकती है |
| बाक़ी 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 करने से रोकती है।
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 में वही:
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 कुल दर को उस अंक के नीचे रखती है:
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 भरता है और असली समस्या छिपाता है।
इसे ऑपरेशन की अवस्था की तरह डिज़ाइन कीजिए, एरर की शाखा की तरह नहीं।
- पहले 402 पर queue को
PAUSED_WALLETचिह्नित कीजिए, paid काम उठाना बंद कीजिए और नया काम queue में लेते रहिए। - किसी इंसान को ऐसे संदेश से सूचित कीजिए जो बताए कि क्या रुका है और कितना इंतज़ार में है।
- यूज़रों को सच्चा संदेश दिखाइए ("हम अभी verification पूरा नहीं कर पा रहे"), verification की विफलता नहीं।
- 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 उसे ऐसा बनाता है।
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 को अपनी डुप्लिकेट-पहचान की कुंजी बनाइए।
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 200300 सेकंड की खिड़की 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
POST /v1/verify/panRequest parameters
| Field | Type | Description |
|---|---|---|
Authorizationrequired | header | Bearer <api-key> (e.g. sk_test_... in sandbox, sk_live_... in production). The x-api-key header is also accepted. |
panrequired | string | PAN, format ABCDE1234F. |
name | string | Name to match against records (max 256 chars). |
POST /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"
}POST /v1/verify/panPAN: ₹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, यह नोट कि
referenceIdidempotency key नहीं है,Idempotency-Keyheader और उसके नियम, और 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)। किसी प्लेटफ़ॉर्म-विशेष का अंक उद्धृत नहीं किया गया।