SMSCode API गाइड — डेवलपर के लिए पूरी जानकारी

SMSCode API गाइड — डेवलपर के लिए पूरी जानकारी

आज के दौर में किसी भी आधुनिक अनुप्रयोग में SMS वेरिफिकेशन एक बुनियादी ज़रूरत बन चुकी है — अकाउंट साइनअप, दो-चरण वेरिफिकेशन, पासवर्ड रिकवरी। लेकिन असली फोन नंबरों से टेस्टिंग करना बहुत उलझन भरा होता है: प्रोडक्शन डेटा का खतरा, टेस्ट अकाउंट की सफाई, अंतर्राष्ट्रीय नंबर की पहुँच — ये सारी परेशानियाँ डेवलपर के अनुभव को बिगाड़ देती हैं।

SMSCode API इन प्रवाहों के लिए Bearer authentication, JSON envelopes, paginated catalog, order lifecycle और signed webhook contract देता है। देश, service, कीमत और stock के बारे में current catalog को authority मानें; guide में किसी स्थिर संख्या पर निर्भर न रहें।

यह गाइड पूरी तरह डेवलपर के नज़रिये से लिखी गई है — API की बनावट से लेकर कोड उदाहरण, सर्वोत्तम तरीके और प्रोडक्शन में तैनाती के पैटर्न तक सब कुछ शामिल है।

TL;DR: SMSCode REST API से किसी भी server-side अनुप्रयोग में वर्चुअल नंबर और OTP की सुविधा जोड़ें। Bearer टोकन प्रमाणीकरण, JSON जवाब और signed webhook समर्थन उपलब्ध हैं। Python, JavaScript और PHP के उदाहरण शामिल हैं। Paid create वास्तविक IDR balance का उपयोग करता है।

SMSCode API क्यों चुनें?

डेवलपर टूल का मूल्यांकन करते समय documented contract, money safety और failure handling पर ध्यान देना ज़रूरी है।

तकनीकी फ़ायदे

आधुनिक REST डिज़ाइन:

  • अनुमानित एंडपॉइंट संरचना — कोई भ्रम नहीं
  • एकसमान JSON जवाब प्रारूप (सफलता और त्रुटि दोनों स्थितियों में)
  • मानक HTTP स्टेटस कोड
  • स्टेटलेस बनावट — क्षैतिज स्केलिंग के लिए उपयुक्त

विश्वसनीय client contract:

  • नेटवर्क और 5xx परिणामों पर paid create को ambiguous मानना
  • caller-owned durable job identity के तहत उसी Idempotency-Key और उसी body से store-clock reconciliation
  • server के expires_at, status और cancel response को authoritative मानना
  • हर order के लिए -1 baseline से केवल strictly newer non-boolean integer sms_revision और nonblank string otp_message process करना, फिर lifecycle status देखना
  • resolved create में phone assignment न मिले तो उसी order ID पर एक bounded GET के बाद अलग pending_assignment लौटाना

डेवलपर के अनुकूल:

  • signed webhook delivery के साथ polling-based reconciliation
  • एकसमान त्रुटि कोड — पूर्वानुमानित एरर हैंडलिंग
  • Python, JavaScript और PHP के लिए तैयार कोड उदाहरण
  • absolute polling deadline के भीतर 429 और optional positive Retry-After के लिए bounded handling

व्यावसायिक फ़ायदे

लागत और उपलब्धता:

  • हर उत्पाद का वर्तमान मूल्य catalog में पूर्णांक IDR के रूप में
  • हर उत्पाद की वर्तमान उपलब्धता catalog के integer available फ़ील्ड में
  • active, price और available को खरीद से पहले दोबारा पढ़ना

Contract checkpoints:

पहलू SMSCode v1 contract
Authentication Authorization: Bearer
Money पूर्णांक IDR
Catalog paginated और runtime-driven
Paid create stable key + exact body
Webhook raw-body HMAC, durable inbox, replay deduplication

अगर आप पहली बार SMSCode के बारे में जानना चाहते हैं, तो वर्चुअल नंबर क्या होता है यह गाइड पहले पढ़ें।

API पहुँच का सेटअप

पहला चरण: अकाउंट बनाएँ

SMSCode.gg पर जाएँ और:

  1. अपना ईमेल पता दर्ज करें
  2. एक मज़बूत पासवर्ड बनाएँ
  3. ईमेल वेरिफाई करें
  4. डैशबोर्ड तक पहुँचें

SMSCode पर रजिस्ट्रेशन कैसे करें की विस्तृत गाइड भी ज़रूर पढ़ें — वहाँ हर कदम की तस्वीरों सहित जानकारी दी गई है।

दूसरा चरण: API टोकन बनाएँ

Dashboard → Account → API अनुभाग में जाएँ:

  1. वर्तमान API token generate करें
  2. टोकन सुरक्षित secret store में रखें
  3. compromise होने पर token regenerate करें; regeneration पुराने token को replace करती है

तीसरा चरण: टोकन को सुरक्षित तरीके से सेव करें

स्रोत कोड में टोकन कभी हार्डकोड न करें। यह एक गंभीर सुरक्षा जोखिम है।

# .env फ़ाइल में सेव करें
SMSCODE_API_TOKEN=your_token_here
# Python — पर्यावरण चर से लोड करें
import os
API_TOKEN = os.environ.get("SMSCODE_API_TOKEN")
if not API_TOKEN:
    raise ValueError("SMSCODE_API_TOKEN not set")
// Node.js — पर्यावरण चर से लोड करें
const API_TOKEN = process.env.SMSCODE_API_TOKEN;
if (!API_TOKEN) {
    throw new Error("SMSCODE_API_TOKEN environment variable required");
}

.gitignore में यह लाइनें ज़रूर जोड़ें:

.env
.env.local
.env.production

इससे आपका टोकन गलती से गिटहब पर अपलोड नहीं होगा।

API की बनावट

आधार URL

https://api.smscode.gg/v1

सभी एंडपॉइंट इसी बेस URL से शुरू होते हैं।

प्रमाणीकरण

Bearer टोकन प्रमाणीकरण — उद्योग का मानक तरीका:

Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json

सभी API अनुरोधों में Authorization हेडर अनिवार्य है। बिना इसके सभी अनुरोध 401 त्रुटि देंगे।

जवाब का प्रारूप

SMSCode एक एकसमान JSON अनुबंध बनाए रखता है — चाहे सफलता हो या विफलता:

सफल जवाब:

{
  "success": true,
  "data": {}
}

त्रुटि जवाब:

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "मानव-पठनीय विवरण"
  }
}

यह एकरूपता त्रुटि प्रबंधन को पूर्वानुमानित और आसान बनाती है।

मुख्य API एंडपॉइंट

GET /v1/catalog/products — सेवाएँ और कीमतें

उपलब्ध सेवाएँ, देश और वर्तमान कीमतें प्राप्त करें:

GET /v1/catalog/products HTTP/1.1
Authorization: Bearer YOUR_API_TOKEN

वैकल्पिक क्वेरी पैरामीटर:

GET /v1/catalog/products?country_id=7&platform_id=1 HTTP/1.1

जवाब:

{
  "success": true,
  "data": [
    {
      "id": 1024,
      "name": "WhatsApp - Indonesia",
      "catalog_product_id": 88,
      "country_id": 7,
      "platform_id": 1,
      "available": 142,
      "price": 15000,
      "active": true
    }
  ],
  "meta": { "page": 1, "limit": 1000, "count": 1 }
}

उपयोग सुझाव: Catalog cache की TTL अपने workload के अनुसार bounded रखें और paid create से पहले ज़रूरत पड़ने पर product को refresh करें। कीमत और उपलब्धता runtime data हैं; उनकी update frequency के बारे में fixed assumption न बनाएं।

POST /v1/orders/create — वर्चुअल नंबर ऑर्डर

POST /v1/orders/create HTTP/1.1
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json
Idempotency-Key: order-example-001

{
  "catalog_product_id": 88,
  "quantity": 1
}

अनुरोध के पैरामीटर:

  • catalog_product_id (अनिवार्य): Catalog से मिला स्थिर उत्पाद ID
  • quantity (वैकल्पिक): इस उदाहरण में एक नंबर

जवाब:

{
  "success": true,
  "data": {
    "orders": [{
      "id": 90210,
      "status": "ACTIVE",
      "phone_number": "+919876543210",
      "otp_code": null,
      "otp_received_at": null,
      "expires_at": "2026-03-16T10:30:00Z",
      "failed_reason": null,
      "product_id": 1024,
      "catalog_product_id": 88,
      "operator_id": null,
      "operator_name": null,
      "amount": 750000,
      "can_finish": false,
      "can_resend": false,
      "can_cancel": false,
      "can_replace": false,
      "can_reactivate": false,
      "resend_available_at": null,
      "cancel_available_at": "2026-03-16T10:02:00Z",
      "replace_available_at": "2026-03-16T10:02:00Z"
    }],
    "failed_count": 0
  }
}

Nonblank string phone_number मिले तभी उसे लक्षित मंच पर इस्तेमाल करें। Resolved create में यह field absent, null या blank हो सकती है; तब उसी resolved order ID पर ठीक एक bounded GET करें और assignment फिर भी मान्य न हो तो create को replay किए बिना pending_assignment लौटाएँ। अस्पष्ट नेटवर्क या 5xx create परिणाम पर यही Idempotency-Key और बिल्कुल यही JSON body दोबारा भेजें; नया key या बदला हुआ body इस्तेमाल न करें।

ज़रूरी बात: ऑर्डर बनते ही बैलेंस से राशि काटी जाती है। अगर server बिना किसी SMS के order को EXPIRED करता है, debit atomically वापस होता है; अगर SMS आ चुकी है तो order बिना refund के final हो सकता है। Local clock से money outcome न मानें—latest snapshot और balance पढ़ें।

GET /v1/orders/{order_id} — OTP स्थिति जाँच

GET /v1/orders/90210 HTTP/1.1
Authorization: Bearer YOUR_API_TOKEN

V1OrderSummary projection, OTP अभी नहीं मिली (चुने हुए fields, पूरी wire response नहीं):

{
  "success": true,
  "data": {
    "id": 90210,
    "phone_number": "+919876543210",
    "status": "ACTIVE",
    "can_cancel": true,
    "expires_at": "2026-03-16T10:30:00Z"
  }
}

V1OrderSummary projection, SMS मिली (चुने हुए fields, पूरी wire response नहीं):

{
  "success": true,
  "data": {
    "id": 90210,
    "phone_number": "+919876543210",
    "status": "OTP_RECEIVED",
    "otp_code": null,
    "otp_message": "Your WhatsApp code: 847291. Don't share this code.",
    "sms_revision": 1,
    "can_cancel": false,
    "otp_received_at": "2026-03-16T10:12:35Z"
  }
}

V1OrderSummary projection, समाप्त (चुने हुए fields, पूरी wire response नहीं):

{
  "success": true,
  "data": {
    "id": 90210,
    "status": "EXPIRED",
    "can_cancel": false
  }
}

हर order का accepted sms_revision -1 से शुरू करें। Snapshot में non-boolean integer revision baseline से strictly greater और otp_message nonblank string हो तो पहले baseline बढ़ाकर message surface करें—OTP_RECEIVED और otp_code इसके gates नहीं हैं। Invalid, पुराना या blank evidence baseline नहीं बदलता; इसके बाद ही COMPLETED, CANCELED या EXPIRED पर polling रोकें।

POST /v1/orders/cancel — ऑर्डर रद्द करें

अगर OTP की ज़रूरत नहीं रही तो केवल नवीनतम order snapshot में can_cancel: true होने पर रद्द करें:

POST /v1/orders/cancel HTTP/1.1
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json

{"id":90210}

जवाब:

{
  "success": true,
  "data": {
    "order_id": 90210,
    "status": "CANCELED",
    "refund_amount": 750000,
    "new_balance": 2000000
  }
}

Client cancellation को केवल पाँच outcomes में बाँटें: skipped, receipt, rejected, confirmed_canceled और ambiguous। केवल 200 success envelope में data.status: CANCELED वाला receipt refund fields दिखा सकता है। Ambiguous POST के बाद GET snapshot में CANCELED केवल state की पुष्टि है; वह cancel receipt नहीं है और उसमें refund fields न जोड़ें।

ऑर्डर कब रद्द करें:

  • मंच ने नंबर अस्वीकार किया
  • उपयोगकर्ता ने प्रक्रिया बीच में छोड़ दी
  • गलत सेवा या देश का ऑर्डर हो गया
  • समय-सीमा से पहले सफाई करनी हो

GET /v1/balance — अकाउंट बैलेंस

GET /v1/balance HTTP/1.1
Authorization: Bearer YOUR_API_TOKEN

जवाब:

{
  "success": true,
  "data": {
    "currency": "IDR",
    "balance": 50000
  }
}

निगरानी सुझाव: बैलेंस सीमा अलर्ट ज़रूर लगाएँ — स्वचालित कम-बैलेंस सूचना आपको प्रोडक्शन में रुकावट से बचाएगी। SMSCode में रिचार्ज कैसे करें — यह गाइड भी पढ़ें।

पूरे कोड उदाहरण

नीचे का CreateAttemptStore साधारण key-value store नहीं है। Caller पहले अपने application-job record में non-secret caller_scope, durable business_job_id, Idempotency-Key और store-clock review_threshold_at सहेजता है। Store में (caller_scope, business_job_id) पर atomic UNIQUE insert-or-load होता है और वही pair immutable endpoint तथा canonical body के fingerprint से बँधा रहता है। Prepare, load, claim और append सभी इसी pair से scoped हैं। Send से पहले store समय-सीमित single-owner lease देता है; हर state change उसी claim token और expected version के साथ append-only CAS event है। Worker sending में रुक जाए तो lease समाप्त होने के बाद अगला worker stored endpoint, body और key को ही replay करता है। Ambiguity store-clock backoff पर durable needs_reconciliation रहती है; caller के persisted review threshold पर job हटती नहीं, बल्कि operator_review में retained रहती है।

Python — प्रोडक्शन-तैयार लागूकरण

import os
import hashlib
import json
import math
import time
import requests
from urllib3.util import Timeout
from typing import Optional, Dict, Any
from myapp.attempts import durable_create_attempt_store

DEFINITIVE_CREATE_ERRORS = {
    "NO_OFFER_AVAILABLE",
    "VALIDATION_ERROR",
    "PROVIDER_ERROR",
    "IDEMPOTENCY_KEY_REUSED",
}
ORDER_STATUSES = {
    "ACTIVE", "OTP_RECEIVED", "COMPLETED", "CANCELED", "EXPIRED"
}
V1_CREATE_ITEM_REQUIRED_FIELDS = {
    "id", "status", "product_id", "amount", "otp_code", "can_finish",
    "can_resend", "can_cancel", "can_replace", "can_reactivate",
    "resend_available_at", "cancel_available_at", "replace_available_at",
}
V1_CREATE_ITEM_FIELDS = V1_CREATE_ITEM_REQUIRED_FIELDS | {
    "phone_number", "otp_received_at", "expires_at", "failed_reason",
    "catalog_product_id", "operator_id", "operator_name",
}
V1_CREATE_NULLABLE_STRING_FIELDS = {
    "phone_number", "otp_code", "otp_received_at", "expires_at",
    "failed_reason", "operator_name", "resend_available_at",
    "cancel_available_at", "replace_available_at",
}
V1_CREATE_BOOLEAN_FIELDS = {
    "can_finish", "can_resend", "can_cancel", "can_replace", "can_reactivate",
}
CANCEL_ERROR_CODES_BY_STATUS = {
    401: {"UNAUTHORIZED"},
    404: {"NOT_FOUND"},
    409: {"CONFLICT", "CANCEL_TOO_EARLY"},
    422: {"PROVIDER_ERROR", "VALIDATION_ERROR"},
    429: {"RATE_LIMIT_EXCEEDED"},
    500: {"INTERNAL_ERROR"},
    503: {"SERVICE_UNAVAILABLE"},
}
CREATE_CONNECT_TIMEOUT_SECONDS = 5
CREATE_TOTAL_TIMEOUT_SECONDS = 30
CREATE_LEASE_SECONDS = 45
RECONCILIATION_BASE_BACKOFF_SECONDS = 2
RECONCILIATION_MAX_BACKOFF_SECONDS = 60
CLIENT_REPLAY_MAX = 3600
MAX_AUTOMATIC_SENDS = 32


def _is_int32(value: Any) -> bool:
    return type(value) is int and -(2 ** 31) <= value <= (2 ** 31) - 1


def _is_int64(value: Any) -> bool:
    return type(value) is int and -(2 ** 63) <= value <= (2 ** 63) - 1


def is_v1_error_response(value: Any) -> bool:
    if (
        type(value) is not dict
        or value.get("success") is not False
        or set(value) != {"success", "error"}
        or type(value["error"]) is not dict
    ):
        return False
    error = value["error"]
    fields = set(error)
    return (
        {"code", "message"} <= fields <= {"code", "message", "details"}
        and type(error["code"]) is str
        and type(error["message"]) is str
        and ("details" not in error or type(error["details"]) is dict)
    )


def validate_v1_create_order_result(value: Any) -> Dict[str, Any]:
    """Closed OpenAPI V1CreateOrderResult को पूरी तरह validate करें।"""
    if type(value) is not dict or set(value) != {"orders", "failed_count"}:
        raise ValueError("V1CreateOrderResult fields invalid हैं")
    if type(value["orders"]) is not list:
        raise ValueError("orders array होना चाहिए")
    if not _is_int32(value["failed_count"]) or value["failed_count"] < 0:
        raise ValueError("failed_count non-negative int32 होना चाहिए")

    for item in value["orders"]:
        if type(item) is not dict:
            raise ValueError("हर order object होना चाहिए")
        fields = set(item)
        if not V1_CREATE_ITEM_REQUIRED_FIELDS <= fields:
            raise ValueError("V1CreateOrderItem का required field गायब है")
        if not fields <= V1_CREATE_ITEM_FIELDS:
            raise ValueError("V1CreateOrderItem में unknown field है")
        if not _is_int32(item["id"]) or not _is_int32(item["product_id"]):
            raise ValueError("order और product IDs int32 होने चाहिए")
        for field in ("catalog_product_id", "operator_id"):
            if field in item and item[field] is not None and not _is_int32(item[field]):
                raise ValueError(f"{field} nullable int32 होना चाहिए")
        if type(item["status"]) is not str or item["status"] not in ORDER_STATUSES:
            raise ValueError("unknown order status")
        if not _is_int64(item["amount"]):
            raise ValueError("amount int64 होना चाहिए")
        for field in V1_CREATE_BOOLEAN_FIELDS:
            if type(item[field]) is not bool:
                raise ValueError(f"{field} boolean होना चाहिए")
        for field in V1_CREATE_NULLABLE_STRING_FIELDS & fields:
            if item[field] is not None and type(item[field]) is not str:
                raise ValueError(f"{field} nullable string होना चाहिए")

    return value


def _valid_order_snapshot(value: Any, order_id: int) -> bool:
    return (
        type(value) is dict
        and _is_int32(value.get("id"))
        and value["id"] == order_id
        and type(value.get("status")) is str
        and value["status"] in ORDER_STATUSES
        and "refund_amount" not in value
        and "new_balance" not in value
    )


def _validated_cancel_receipt(payload: Any, order_id: int) -> Optional[Dict]:
    if type(payload) is not dict or payload.get("success") is not True:
        return None
    if not set(payload) <= {"success", "data", "meta"} or "data" not in payload:
        return None
    data = payload["data"]
    if type(data) is not dict or set(data) != {
        "order_id", "status", "refund_amount", "new_balance"
    }:
        return None
    if not _is_int32(data["order_id"]) or data["order_id"] != order_id:
        return None
    if data["status"] != "CANCELED":
        return None
    if not _is_int64(data["refund_amount"]) or data["refund_amount"] < 0:
        return None
    if not _is_int64(data["new_balance"]):
        return None
    return data


def _documented_cancel_error(http_status: int, payload: Any) -> Optional[Dict]:
    if type(payload) is not dict or set(payload) != {"success", "error"}:
        return None
    error = payload.get("error")
    if payload["success"] is not False or type(error) is not dict:
        return None
    if not {"code", "message"} <= set(error) or not set(error) <= {
        "code", "message", "details"
    }:
        return None
    if type(error["code"]) is not str or type(error["message"]) is not str:
        return None
    if "details" in error and type(error["details"]) is not dict:
        return None
    if error["code"] not in CANCEL_ERROR_CODES_BY_STATUS.get(http_status, set()):
        return None
    return error

class SMSCodeClient:
    BASE_URL = "https://api.smscode.gg/v1"

    def __init__(self, api_token: str, attempt_store):
        self.headers = {
            "Authorization": f"Bearer {api_token}",
            "Content-Type": "application/json"
        }
        self.attempt_store = attempt_store

    def _raw_request(self, method: str, endpoint: str, **kwargs):
        url = f"{self.BASE_URL}{endpoint}"
        request_headers = {**self.headers, **kwargs.pop("headers", {})}
        kwargs.setdefault(
            "timeout",
            Timeout(
                connect=CREATE_CONNECT_TIMEOUT_SECONDS,
                total=CREATE_TOTAL_TIMEOUT_SECONDS,
            ),
        )
        return requests.request(method, url, headers=request_headers, **kwargs)

    @staticmethod
    def _decode_json(response) -> Any:
        return json.loads(response.content.decode("utf-8", errors="strict"))

    def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
        """5s connect और 30s total deadline के साथ API अनुरोध करें।"""
        response = self._raw_request(method, endpoint, **kwargs)
        data = self._decode_json(response)
        if (
            response.status_code != 200
            or type(data) is not dict
            or data.get("success") is not True
            or "data" not in data
            or "error" in data
        ):
            error = data.get("error", {}) if type(data) is dict else {}
            error = error if type(error) is dict else {}
            raise Exception(f"API Error {error.get('code')}: {error.get('message')}")
        return data["data"]

    def get_catalog(self, country_id: int, platform_id: int):
        """उपलब्ध सेवाएँ और कीमतें प्राप्त करें"""
        return self._request(
            "GET",
            "/catalog/products",
            params={"country_id": country_id, "platform_id": platform_id},
        )

    def prepare_create_attempt(
        self,
        caller_scope: str,
        business_job_id: str,
        idempotency_key: str,
        review_threshold_at: int,
        catalog_product_id: int,
        quantity: int = 1,
    ) -> Dict:
        """Caller-owned identity से वही durable attempt बनाएँ या वापस पाएँ।"""
        if not caller_scope or not business_job_id or not idempotency_key:
            raise ValueError("caller identity और persisted key अनिवार्य हैं")
        if type(review_threshold_at) is not int:
            raise ValueError("review_threshold_at store-clock timestamp होना चाहिए")
        if not _is_int32(catalog_product_id) or not _is_int32(quantity) or quantity <= 0:
            raise ValueError("catalog_product_id int32 होना चाहिए")
        body = {"catalog_product_id": catalog_product_id, "quantity": quantity}
        endpoint = "/orders/create"
        body_json = json.dumps(body, separators=(",", ":"), sort_keys=True)
        request_fingerprint = hashlib.sha256(
            f"{endpoint}\0{body_json}".encode("utf-8")
        ).hexdigest()
        started_at = self.attempt_store.now()
        candidate = {
            "caller_scope": caller_scope,
            "business_job_id": business_job_id,
            "endpoint": endpoint,
            "body_json": body_json,
            "request_fingerprint": request_fingerprint,
            "idempotency_key": idempotency_key,
            "review_threshold_at": review_threshold_at,
            "started_at": started_at,
            "effective_replay_deadline_at": min(review_threshold_at, started_at + CLIENT_REPLAY_MAX),
            "sends": 0,
            "attempt_history": [],
        }
        # Existing pair पर fingerprint, key या review threshold अलग हो तो
        # atomic conflict हो; caller का नया candidate चुपचाप स्वीकार न करें।
        attempt = self.attempt_store.insert_or_load(
            caller_scope,
            business_job_id,
            request_fingerprint,
            candidate,
        )
        self._assert_attempt_binding(
            attempt,
            caller_scope,
            business_job_id,
            idempotency_key,
            review_threshold_at,
        )
        return attempt

    @staticmethod
    def _assert_attempt_binding(
        attempt: Dict,
        caller_scope: str,
        business_job_id: str,
        idempotency_key: str,
        review_threshold_at: int,
    ) -> None:
        fingerprint = hashlib.sha256(
            f"{attempt['endpoint']}\0{attempt['body_json']}".encode("utf-8")
        ).hexdigest()
        if (type(attempt.get("started_at")) is not int or
            attempt.get("effective_replay_deadline_at") != min(
                review_threshold_at, attempt["started_at"] + CLIENT_REPLAY_MAX
            ) or type(attempt.get("sends")) is not int or attempt["sends"] < 0):
            raise ValueError("Stored replay bounds invalid हैं")
        expected = {
            "caller_scope": caller_scope,
            "business_job_id": business_job_id,
            "idempotency_key": idempotency_key,
            "review_threshold_at": review_threshold_at,
            "endpoint": "/orders/create",
            "request_fingerprint": fingerprint,
        }
        if any(attempt.get(field) != value for field, value in expected.items()):
            raise ValueError("Stored create attempt caller identity से मेल नहीं खाता")

    def _append_create_event(
        self,
        caller_scope: str,
        business_job_id: str,
        attempt: Dict,
        claim_token: str,
        event: Dict,
    ) -> Dict:
        # CAS append terminal पर lease छोड़ता है; needs_reconciliation पर lease
        # छोड़कर persisted next_attempt_at को due queue में रखता है।
        return self.attempt_store.append_event(
            caller_scope,
            business_job_id,
            claim_token=claim_token,
            expected_version=attempt["version"],
            event=event,
        )

    def _recovery_fields(self, attempt: Dict) -> Dict:
        return {
            "endpoint": attempt["endpoint"],
            "body_json": attempt["body_json"],
            "idempotency_key": attempt["idempotency_key"],
            "review_threshold_at": attempt["review_threshold_at"],
            "effective_replay_deadline_at": attempt["effective_replay_deadline_at"],
            "sends": attempt.get("sends", 0),
        }

    def _backoff_seconds(self, attempt: Dict) -> int:
        send_count = attempt.get("sends", 0)
        exponent = min(max(send_count - 1, 0), 5)
        return min(
            RECONCILIATION_BASE_BACKOFF_SECONDS * (2 ** exponent),
            RECONCILIATION_MAX_BACKOFF_SECONDS,
        )

    def _needs_reconciliation(
        self,
        caller_scope: str,
        business_job_id: str,
        attempt: Dict,
        claim_token: str,
        reason: str,
        **evidence,
    ) -> Dict:
        event = {
            "state": "needs_reconciliation",
            "reason": reason,
            **self._recovery_fields(attempt),
            **evidence,
        }
        event["next_attempt_at"] = min(
            self.attempt_store.now() + self._backoff_seconds(attempt),
            attempt["effective_replay_deadline_at"],
        )
        attempt = self._append_create_event(
            caller_scope, business_job_id, attempt, claim_token, event
        )
        return {"kind": "needs_reconciliation", "attempt": attempt}

    def create_order(
        self,
        caller_scope: str,
        business_job_id: str,
        idempotency_key: str,
        review_threshold_at: int,
        attempt: Dict,
    ) -> Dict:
        """एक claim में एक send करें; retry durable worker बाद में चलाता है।"""
        self._assert_attempt_binding(
            attempt, caller_scope, business_job_id,
            idempotency_key, review_threshold_at,
        )
        pre_now = self.attempt_store.now()
        if pre_now >= attempt["effective_replay_deadline_at"] or attempt.get("sends", 0) >= MAX_AUTOMATIC_SENDS:
            return {"kind": "operator_review", "attempt": self.attempt_store.retain_operator_review(caller_scope, business_job_id, attempt, pre_now)}
        claimed = self.attempt_store.claim_for_send(
            caller_scope,
            business_job_id,
            expected_version=attempt["version"],
            lease_seconds=CREATE_LEASE_SECONDS,
        )
        attempt = claimed["attempt"]
        claim_token = claimed["claim_token"]
        self._assert_attempt_binding(
            attempt, caller_scope, business_job_id,
            idempotency_key, review_threshold_at,
        )

        store_now = self.attempt_store.now()
        if store_now >= attempt["effective_replay_deadline_at"] or attempt.get("sends", 0) >= MAX_AUTOMATIC_SENDS:
            attempt = self._append_create_event(
                caller_scope,
                business_job_id,
                attempt,
                claim_token,
                {
                    "state": "operator_review",
                    "retained_at": store_now,
                    **self._recovery_fields(attempt),
                },
            )
            return {"kind": "operator_review", "attempt": attempt}

        send_number = attempt.get("sends", 0) + 1
        attempt = self._append_create_event(
            caller_scope,
            business_job_id,
            attempt,
            claim_token,
            {**self._recovery_fields(attempt),
                "number": send_number,
                "state": "sending",
                "last_send_at": store_now,
                "deadline_at": store_now + CREATE_TOTAL_TIMEOUT_SECONDS,
                "sends": send_number,
            },
        )
        try:
            response = self._raw_request(
                "POST",
                attempt["endpoint"],
                data=attempt["body_json"],
                headers={"Idempotency-Key": attempt["idempotency_key"]},
            )
        except requests.RequestException:
            return self._needs_reconciliation(
                caller_scope, business_job_id, attempt, claim_token, "transport"
            )

        if response.status_code >= 500:
            return self._needs_reconciliation(
                caller_scope,
                business_job_id,
                attempt,
                claim_token,
                "http_5xx",
                http_status=response.status_code,
            )

        try:
            payload = self._decode_json(response)
        except (UnicodeDecodeError, ValueError):
            return self._needs_reconciliation(
                caller_scope,
                business_job_id,
                attempt,
                claim_token,
                "malformed_response",
                http_status=response.status_code,
            )

        if type(payload) is dict and payload.get("success") is False:
            error = payload.get("error")
            code = error.get("code") if type(error) is dict else None
            code = code if type(code) is str else None
            if not is_v1_error_response(payload):
                return self._needs_reconciliation(
                    caller_scope,
                    business_job_id,
                    attempt,
                    claim_token,
                    "malformed_response",
                    http_status=response.status_code,
                    error_code=code,
                )
            if response.status_code == 422 and code in DEFINITIVE_CREATE_ERRORS:
                attempt = self._append_create_event(
                    caller_scope,
                    business_job_id,
                    attempt,
                    claim_token,
                    {
                        "state": "definitive_rejection",
                        "http_status": response.status_code,
                        "error_code": code,
                    },
                )
                return {"kind": "definitive_rejection", "attempt": attempt, "code": code}
            if response.status_code == 409 and code == "INSUFFICIENT_BALANCE":
                attempt = self._append_create_event(
                    caller_scope,
                    business_job_id,
                    attempt,
                    claim_token,
                    {
                        "state": "insufficient_balance",
                        "http_status": response.status_code,
                        "error_code": code,
                    },
                )
                return {"kind": "insufficient_balance", "attempt": attempt}
            reason = (
                "request_in_progress"
                if response.status_code == 409 and code == "REQUEST_IN_PROGRESS"
                else "unknown_response"
            )
            return self._needs_reconciliation(
                caller_scope,
                business_job_id,
                attempt,
                claim_token,
                reason,
                http_status=response.status_code,
                error_code=code,
            )

        if (
            response.status_code != 200
            or type(payload) is not dict
            or payload.get("success") is not True
            or "data" not in payload
            or "error" in payload
        ):
            return self._needs_reconciliation(
                caller_scope,
                business_job_id,
                attempt,
                claim_token,
                "contradictory_response",
                http_status=response.status_code,
            )

        try:
            validated_result = validate_v1_create_order_result(payload["data"])
            quantity = json.loads(attempt["body_json"])["quantity"]
            if len(validated_result["orders"]) + validated_result["failed_count"] != quantity:
                raise ValueError("quantity conservation violated")
        except ValueError:
            return self._needs_reconciliation(
                caller_scope,
                business_job_id,
                attempt,
                claim_token,
                "malformed_success",
                http_status=response.status_code,
            )

        attempt = self._append_create_event(
            caller_scope,
            business_job_id,
            attempt,
            claim_token,
            {"state": "resolved", "validated_response": validated_result},
        )
        return {
            "kind": "resolved",
            "attempt": attempt,
            "result": validated_result,
        }

    def reconcile_create_order(
        self,
        caller_scope: str,
        business_job_id: str,
        idempotency_key: str,
        review_threshold_at: int,
    ) -> Dict:
        """Due durable job को exact stored key/body से replay करें।"""
        attempt = self.attempt_store.load(caller_scope, business_job_id)
        self._assert_attempt_binding(
            attempt, caller_scope, business_job_id,
            idempotency_key, review_threshold_at,
        )
        return self.create_order(
            caller_scope,
            business_job_id,
            idempotency_key,
            review_threshold_at,
            attempt,
        )

    def get_order(self, order_id: int) -> Dict:
        """ऑर्डर की स्थिति जाँचें"""
        return self._request("GET", f"/orders/{order_id}")

    def _reconcile_cancellation(self, order_id: int) -> Dict:
        try:
            latest = self.get_order(order_id)
        except Exception:
            return {"kind": "ambiguous", "order_id": order_id, "latest_status": None}
        if _valid_order_snapshot(latest, order_id) and latest["status"] == "CANCELED":
            return {
                "kind": "confirmed_canceled",
                "snapshot": {"id": order_id, "status": "CANCELED"},
            }
        latest_status = latest.get("status") if _valid_order_snapshot(latest, order_id) else None
        return {
            "kind": "ambiguous",
            "order_id": order_id,
            "latest_status": latest_status,
        }

    def cancel_order(self, order_id: int) -> Dict:
        """केवल skipped/receipt/rejected/confirmed_canceled/ambiguous लौटाएँ।"""
        try:
            current = self.get_order(order_id)
        except Exception:
            return {"kind": "ambiguous", "order_id": order_id, "latest_status": None}
        if not _valid_order_snapshot(current, order_id) or type(current.get("can_cancel")) is not bool:
            return {"kind": "ambiguous", "order_id": order_id, "latest_status": None}
        if not current["can_cancel"]:
            return {"kind": "skipped", "order_id": order_id}

        try:
            response = self._raw_request("POST", "/orders/cancel", json={"id": order_id})
        except requests.RequestException:
            return self._reconcile_cancellation(order_id)
        try:
            payload = self._decode_json(response)
        except (UnicodeDecodeError, ValueError):
            return self._reconcile_cancellation(order_id)

        if response.status_code == 200:
            receipt = _validated_cancel_receipt(payload, order_id)
            if receipt is not None:
                return {"kind": "receipt", "receipt": receipt}
            return self._reconcile_cancellation(order_id)

        error = _documented_cancel_error(response.status_code, payload)
        if error is not None:
            return {
                "kind": "rejected",
                "http_status": response.status_code,
                "error_code": error["code"],
            }
        return self._reconcile_cancellation(order_id)

    def wait_for_otp(
        self,
        order_id: int,
        timeout_seconds: int = 240,
        poll_interval: int = 10
    ) -> Optional[str]:
        """समय-सीमा के साथ OTP के लिए पोल करें"""
        if (
            isinstance(timeout_seconds, bool)
            or not isinstance(timeout_seconds, (int, float))
            or not math.isfinite(timeout_seconds)
        ):
            raise ValueError("timeout_seconds must be a finite number")
        # `timeout_seconds` bounds when this loop stops STARTING new polls; it is
        # not an absolute elapsed-time guarantee, because a GET already in flight
        # still runs to its own transport bound. Never a poll count: each pass also
        # spends request time, so a fixed count would overrun the window outright.
        # A non-positive interval would turn this into a hot loop
        # issuing GETs for the whole budget. Clamp to a documented minimum.
        poll_interval = max(1, int(poll_interval or 0))
        deadline = time.time() + timeout_seconds
        last_seen_revision = -1
        poll_number = 0

        while True:
            poll_number += 1
            order = self.get_order(order_id)
            status = order["status"]

            revision = order.get("sms_revision")
            message = order.get("otp_message")
            if (
                type(revision) is int
                and revision > last_seen_revision
                and isinstance(message, str)
                and message.strip()
            ):
                last_seen_revision = revision
                print(f"SMS revision {revision}: {message}")
                return message
            if status in ("COMPLETED", "EXPIRED", "CANCELED"):
                return None

            print(f"प्रयास {poll_number}: अभी प्रतीक्षारत...")

            # The first poll always runs, even for a non-positive budget: you asked
            # to wait zero seconds, not to skip looking. Every LATER poll is gated
            # after the sleep, so no GET starts once the budget is spent.
            time.sleep(min(poll_interval, max(0.0, deadline - time.time())))
            if time.time() >= deadline:
                break

        # Local timeout money outcome तय नहीं करता; latest snapshot से cancelability जाँचें.
        cancellation = self.cancel_order(order_id)
        if cancellation["kind"] == "receipt":
            receipt = cancellation["receipt"]
            print(
                f"रद्द: refund Rp {receipt['refund_amount']}; "
                f"नया balance Rp {receipt['new_balance']}"
            )
        elif cancellation["kind"] == "confirmed_canceled":
            print("Latest snapshot में CANCELED है; cancel receipt या refund प्रमाण उपलब्ध नहीं है")
        elif cancellation["kind"] == "rejected":
            print(f"Cancellation rejected: {cancellation['error_code']}")
        elif cancellation["kind"] == "ambiguous":
            print("Cancellation unresolved है; durable reconciliation में retained रखें")
        else:
            print("Latest snapshot में can_cancel=false था; cancel POST नहीं भेजा")
        return None


# इस्तेमाल का उदाहरण
def verify_whatsapp(
    client: SMSCodeClient,
    caller_scope: str,
    business_job_id: str,
    idempotency_key: str,
    review_threshold_at: int,
):
    """पूरा WhatsApp वेरिफिकेशन प्रवाह"""
    print("WhatsApp India नंबर के लिए ऑर्डर बना रहे हैं...")
    attempt = client.prepare_create_attempt(
        caller_scope,
        business_job_id,
        idempotency_key,
        review_threshold_at,
        88,
    )
    create_outcome = client.create_order(
        caller_scope,
        business_job_id,
        idempotency_key,
        review_threshold_at,
        attempt,
    )
    if create_outcome["kind"] != "resolved":
        print(f"Create durable state: {create_outcome['kind']}")
        return {"create_outcome": create_outcome["kind"], "attempt": create_outcome["attempt"]}
    create_result = create_outcome["result"]
    if len(create_result["orders"]) != 1 or create_result["failed_count"] != 0:
        return {"create_outcome": "resolved_without_single_order", "attempt": create_outcome["attempt"]}
    order = create_result["orders"][0]

    order_id = order["id"]
    status = order["status"]
    assignment_status = status
    phone = order.get("phone_number")
    if not (isinstance(phone, str) and phone.strip()):
        try:
            current_order = client.get_order(order_id)
        except Exception:
            current_order = None

        current_snapshot_is_valid = _valid_order_snapshot(current_order, order_id)
        if current_snapshot_is_valid:
            assignment_status = current_order["status"]
        current_phone = (
            current_order.get("phone_number")
            if isinstance(current_order, dict)
            else None
        )
        if (
            not current_snapshot_is_valid
            or not isinstance(current_phone, str)
            or not current_phone.strip()
        ):
            return {
                "kind": "pending_assignment",
                "create_outcome": "resolved",
                "order_id": order_id,
                "status": assignment_status,
            }
        phone = current_phone

    print(f"वर्चुअल नंबर: {phone}")

    # TODO: यह फोन नंबर WhatsApp रजिस्ट्रेशन में डालें
    # [यहाँ आपका WhatsApp रजिस्ट्रेशन ऑटोमेशन]

    print("OTP का इंतज़ार हो रहा है...")
    otp = client.wait_for_otp(order_id)

    if otp:
        print(f"OTP मिली: {otp}")
        # TODO: WhatsApp में OTP दर्ज करें
        return {"phone": phone, "otp": otp}
    else:
        print("समय-सीमा के भीतर OTP नहीं मिली")
        return None


# मुख्य प्रवेश बिंदु
api_token = os.environ.get("SMSCODE_API_TOKEN")
client = SMSCodeClient(api_token, durable_create_attempt_store)
result = verify_whatsapp(
    client,
    os.environ["SMSCODE_CALLER_SCOPE"],
    os.environ["SMSCODE_BUSINESS_JOB_ID"],
    os.environ["SMSCODE_IDEMPOTENCY_KEY"],
    int(os.environ["SMSCODE_REVIEW_THRESHOLD_AT"]),
)

JavaScript / Node.js — Async/Await पैटर्न

const crypto = require('crypto');
const { Agent, request } = require('undici');
const { durableCreateAttemptStore } = require('./durable-create-attempt-store');

const DEFINITIVE_CREATE_ERRORS = new Set([
    'NO_OFFER_AVAILABLE',
    'VALIDATION_ERROR',
    'PROVIDER_ERROR',
    'IDEMPOTENCY_KEY_REUSED'
]);
const ORDER_STATUSES = new Set([
    'ACTIVE', 'OTP_RECEIVED', 'COMPLETED', 'CANCELED', 'EXPIRED'
]);
const V1_CREATE_REQUIRED_FIELDS = new Set([
    'id', 'status', 'product_id', 'amount', 'otp_code', 'can_finish',
    'can_resend', 'can_cancel', 'can_replace', 'can_reactivate',
    'resend_available_at', 'cancel_available_at', 'replace_available_at'
]);
const V1_CREATE_FIELDS = new Set([
    ...V1_CREATE_REQUIRED_FIELDS,
    'phone_number', 'otp_received_at', 'expires_at', 'failed_reason',
    'catalog_product_id', 'operator_id', 'operator_name'
]);
const V1_CREATE_NULLABLE_STRING_FIELDS = new Set([
    'phone_number', 'otp_code', 'otp_received_at', 'expires_at',
    'failed_reason', 'operator_name', 'resend_available_at',
    'cancel_available_at', 'replace_available_at'
]);
const V1_CREATE_BOOLEAN_FIELDS = new Set([
    'can_finish', 'can_resend', 'can_cancel', 'can_replace', 'can_reactivate'
]);
const CANCEL_ERROR_CODES_BY_STATUS = new Map([
    [401, new Set(['UNAUTHORIZED'])],
    [404, new Set(['NOT_FOUND'])],
    [409, new Set(['CONFLICT', 'CANCEL_TOO_EARLY'])],
    [422, new Set(['PROVIDER_ERROR', 'VALIDATION_ERROR'])],
    [429, new Set(['RATE_LIMIT_EXCEEDED'])],
    [500, new Set(['INTERNAL_ERROR'])],
    [503, new Set(['SERVICE_UNAVAILABLE'])]
]);
const CREATE_CONNECT_TIMEOUT_MS = 5000;
const CREATE_TOTAL_TIMEOUT_MS = 30000;
const CREATE_LEASE_MS = 45000;
const RECONCILIATION_BASE_BACKOFF_MS = 2000;
const RECONCILIATION_MAX_BACKOFF_MS = 60 * 1000;
const CLIENT_REPLAY_MAX = 3600;
const MAX_AUTOMATIC_SENDS = 32;
const createDispatcher = new Agent({ connectTimeout: CREATE_CONNECT_TIMEOUT_MS });

function canonicalJson(value) {
    if (Array.isArray(value)) {
        return `[${value.map(canonicalJson).join(',')}]`;
    }
    if (value !== null && typeof value === 'object') {
        return `{${Object.keys(value).sort().map((key) =>
            `${JSON.stringify(key)}:${canonicalJson(value[key])}`
        ).join(',')}}`;
    }
    return JSON.stringify(value);
}

function isObject(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function isV1ErrorResponse(value) {
    if (!isObject(value) || value.success !== false ||
        Object.keys(value).sort().join(',') !== 'error,success' ||
        !isObject(value.error)) {
        return false;
    }
    const errorFields = Object.keys(value.error).sort().join(',');
    return (errorFields === 'code,message' || errorFields === 'code,details,message') &&
        typeof value.error.code === 'string' &&
        typeof value.error.message === 'string' &&
        (!('details' in value.error) || isObject(value.error.details));
}

function isInt32(value) {
    return Number.isInteger(value) && value >= -(2 ** 31) && value <= (2 ** 31) - 1;
}

function validateV1CreateOrderResult(value) {
    if (!isObject(value) || Object.keys(value).sort().join(',') !== 'failed_count,orders') {
        throw new Error('V1CreateOrderResult fields invalid हैं');
    }
    if (!Array.isArray(value.orders)) throw new Error('orders array होना चाहिए');
    if (!isInt32(value.failed_count) || value.failed_count < 0) {
        throw new Error('failed_count non-negative int32 होना चाहिए');
    }
    for (const item of value.orders) {
        if (!isObject(item)) throw new Error('हर order object होना चाहिए');
        const fields = new Set(Object.keys(item));
        if ([...V1_CREATE_REQUIRED_FIELDS].some((field) => !fields.has(field))) {
            throw new Error('V1CreateOrderItem का required field गायब है');
        }
        if ([...fields].some((field) => !V1_CREATE_FIELDS.has(field))) {
            throw new Error('V1CreateOrderItem में unknown field है');
        }
        if (!isInt32(item.id) || !isInt32(item.product_id)) {
            throw new Error('order और product IDs int32 होने चाहिए');
        }
        for (const field of ['catalog_product_id', 'operator_id']) {
            if (fields.has(field) && item[field] !== null && !isInt32(item[field])) {
                throw new Error(`${field} nullable int32 होना चाहिए`);
            }
        }
        if (!ORDER_STATUSES.has(item.status)) throw new Error('unknown order status');
        if (!Number.isSafeInteger(item.amount)) throw new Error('amount integer होना चाहिए');
        for (const field of V1_CREATE_BOOLEAN_FIELDS) {
            if (typeof item[field] !== 'boolean') throw new Error(`${field} boolean होना चाहिए`);
        }
        for (const field of V1_CREATE_NULLABLE_STRING_FIELDS) {
            if (fields.has(field) && item[field] !== null && typeof item[field] !== 'string') {
                throw new Error(`${field} nullable string होना चाहिए`);
            }
        }
    }
    return value;
}

function validOrderSnapshot(value, orderId) {
    return isObject(value) && isInt32(value.id) && value.id === orderId &&
        ORDER_STATUSES.has(value.status) && !('refund_amount' in value) &&
        !('new_balance' in value);
}

function validatedCancelReceipt(payload, orderId) {
    if (!isObject(payload) || payload.success !== true || !('data' in payload)) return null;
    if (Object.keys(payload).some((field) => !['success', 'data', 'meta'].includes(field))) {
        return null;
    }
    const data = payload.data;
    if (!isObject(data) || Object.keys(data).sort().join(',') !==
        'new_balance,order_id,refund_amount,status') return null;
    if (!isInt32(data.order_id) || data.order_id !== orderId || data.status !== 'CANCELED') {
        return null;
    }
    if (!Number.isSafeInteger(data.refund_amount) || data.refund_amount < 0 ||
        !Number.isSafeInteger(data.new_balance)) return null;
    return data;
}

function documentedCancelError(httpStatus, payload) {
    if (!isObject(payload) || Object.keys(payload).sort().join(',') !== 'error,success' ||
        payload.success !== false || !isObject(payload.error)) return null;
    const error = payload.error;
    const fields = Object.keys(error);
    if (!fields.includes('code') || !fields.includes('message') ||
        fields.some((field) => !['code', 'message', 'details'].includes(field))) return null;
    if (typeof error.code !== 'string' || typeof error.message !== 'string') return null;
    if ('details' in error && !isObject(error.details)) return null;
    return CANCEL_ERROR_CODES_BY_STATUS.get(httpStatus)?.has(error.code) ? error : null;
}

class SMSCodeClient {
    constructor(apiToken, attemptStore) {
        this.baseUrl = 'https://api.smscode.gg/v1';
        this.headers = {
            'Authorization': `Bearer ${apiToken}`,
            'Content-Type': 'application/json'
        };
        this.attemptStore = attemptStore;
    }

    async _rawRequest(method, endpoint, options = {}) {
        const query = options.params
            ? `?${new URLSearchParams(options.params)}`
            : '';
        return request(`${this.baseUrl}${endpoint}${query}`, {
            method,
            headers: { ...this.headers, ...(options.headers || {}) },
            body: options.body ??
                (options.data !== undefined ? JSON.stringify(options.data) : undefined),
            dispatcher: createDispatcher,
            signal: AbortSignal.timeout(CREATE_TOTAL_TIMEOUT_MS)
        });
    }

    static async _decodeJson(response) {
        const bytes = new Uint8Array(await response.body.arrayBuffer());
        return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
    }

    async _request(method, endpoint, options = {}) {
        const response = await this._rawRequest(method, endpoint, options);
        const data = await SMSCodeClient._decodeJson(response);
        if (response.statusCode !== 200 || !isObject(data) || data.success !== true ||
            !('data' in data) || 'error' in data) {
            const { code, message } = data.error || {};
            throw new Error(`API Error ${code}: ${message}`);
        }
        return data.data;
    }

    async getCatalog(countryId, platformId) {
        return this._request('GET', '/catalog/products', {
            params: { country_id: countryId, platform_id: platformId }
        });
    }

    async prepareCreateAttempt(
        callerScope,
        businessJobId,
        idempotencyKey,
        reviewThresholdAt,
        catalogProductId,
        quantity = 1
    ) {
        if (!callerScope || !businessJobId || !idempotencyKey ||
            !Number.isInteger(reviewThresholdAt) || !isInt32(catalogProductId) ||
            !isInt32(quantity) || quantity <= 0) {
            throw new Error('Caller identity, persisted key, threshold और int32 product अनिवार्य हैं');
        }
        const body = { catalog_product_id: catalogProductId, quantity };
        const endpoint = '/orders/create';
        const bodyJson = canonicalJson(body);
        const requestFingerprint = crypto.createHash('sha256')
            .update(endpoint).update('\0').update(bodyJson).digest('hex');
        const startedAt = this.attemptStore.now();
        const candidate = {
            callerScope,
            businessJobId,
            endpoint,
            bodyJson,
            requestFingerprint,
            idempotencyKey,
            reviewThresholdAt,
            startedAt,
            effectiveReplayDeadlineAt: Math.min(reviewThresholdAt, startedAt + CLIENT_REPLAY_MAX * 1000),
            sends: 0,
            attemptHistory: []
        };
        // Existing pair पर fingerprint, key या threshold अलग हो तो conflict करें।
        const attempt = await this.attemptStore.insertOrLoad(
            callerScope,
            businessJobId,
            requestFingerprint,
            candidate
        );
        this.assertAttemptBinding(
            attempt, callerScope, businessJobId, idempotencyKey, reviewThresholdAt
        );
        return attempt;
    }

    assertAttemptBinding(
        attempt,
        callerScope,
        businessJobId,
        idempotencyKey,
        reviewThresholdAt
    ) {
        const fingerprint = crypto.createHash('sha256')
            .update(attempt.endpoint).update('\0').update(attempt.bodyJson).digest('hex');
        if (!Number.isInteger(attempt.startedAt) ||
            attempt.effectiveReplayDeadlineAt !== Math.min(
                reviewThresholdAt, attempt.startedAt + CLIENT_REPLAY_MAX * 1000
            ) || !Number.isInteger(attempt.sends) || attempt.sends < 0) {
            throw new Error('Stored replay bounds invalid हैं');
        }
        if (attempt.callerScope !== callerScope ||
            attempt.businessJobId !== businessJobId ||
            attempt.idempotencyKey !== idempotencyKey ||
            attempt.reviewThresholdAt !== reviewThresholdAt ||
            attempt.endpoint !== '/orders/create' ||
            attempt.requestFingerprint !== fingerprint) {
            throw new Error('Stored create attempt caller identity से मेल नहीं खाता');
        }
    }

    async appendCreateEvent(
        callerScope,
        businessJobId,
        attempt,
        claimToken,
        event
    ) {
        // Store CAS append करता है; reconciliation event lease छोड़कर due queue बनाता है।
        return this.attemptStore.appendEvent(callerScope, businessJobId, {
            claimToken,
            expectedVersion: attempt.version,
            event
        });
    }

    recoveryFields(attempt) {
        return {
            endpoint: attempt.endpoint,
            bodyJson: attempt.bodyJson,
            idempotencyKey: attempt.idempotencyKey,
            reviewThresholdAt: attempt.reviewThresholdAt
            , effectiveReplayDeadlineAt: attempt.effectiveReplayDeadlineAt, sends: attempt.sends || 0
        };
    }

    backoffMs(attempt) {
        const sendCount = attempt.sends || 0;
        const exponent = Math.min(Math.max(sendCount - 1, 0), 5);
        return Math.min(
            RECONCILIATION_BASE_BACKOFF_MS * (2 ** exponent),
            RECONCILIATION_MAX_BACKOFF_MS
        );
    }

    async needsReconciliation(
        callerScope,
        businessJobId,
        attempt,
        claimToken,
        reason,
        evidence = {}
    ) {
        const event = {
            state: 'needs_reconciliation',
            reason,
            ...this.recoveryFields(attempt),
            ...evidence
        };
        event.nextAttemptAt = Math.min(
            this.attemptStore.now() + this.backoffMs(attempt),
            attempt.effectiveReplayDeadlineAt
        );
        attempt = await this.appendCreateEvent(
            callerScope, businessJobId, attempt, claimToken, event
        );
        return { kind: 'needs_reconciliation', attempt };
    }

    async createOrder(
        callerScope,
        businessJobId,
        idempotencyKey,
        reviewThresholdAt,
        attempt
    ) {
        this.assertAttemptBinding(
            attempt, callerScope, businessJobId, idempotencyKey, reviewThresholdAt
        );
        const preNow = this.attemptStore.now();
        if (preNow >= attempt.effectiveReplayDeadlineAt || (attempt.sends || 0) >= MAX_AUTOMATIC_SENDS) {
            return { kind: 'operator_review', attempt: await this.attemptStore.retainOperatorReview(callerScope, businessJobId, attempt, preNow) };
        }
        const claimed = await this.attemptStore.claimForSend(
            callerScope,
            businessJobId,
            { expectedVersion: attempt.version, leaseMs: CREATE_LEASE_MS }
        );
        attempt = claimed.attempt;
        const claimToken = claimed.claimToken;
        this.assertAttemptBinding(
            attempt, callerScope, businessJobId, idempotencyKey, reviewThresholdAt
        );

        const storeNow = this.attemptStore.now();
        if (storeNow >= attempt.effectiveReplayDeadlineAt || (attempt.sends || 0) >= MAX_AUTOMATIC_SENDS) {
            attempt = await this.appendCreateEvent(
                callerScope,
                businessJobId,
                attempt,
                claimToken,
                {
                    state: 'operator_review',
                    retainedAt: storeNow,
                    ...this.recoveryFields(attempt)
                }
            );
            return { kind: 'operator_review', attempt };
        }

        const sendNumber = (attempt.sends || 0) + 1;
        attempt = await this.appendCreateEvent(
            callerScope,
            businessJobId,
            attempt,
            claimToken,
            {
                ...this.recoveryFields(attempt),
                number: sendNumber,
                state: 'sending',
                lastSendAt: storeNow,
                deadlineAt: storeNow + CREATE_TOTAL_TIMEOUT_MS,
                sends: sendNumber
            }
        );

        let response;
        try {
            response = await this._rawRequest('POST', attempt.endpoint, {
                headers: { 'Idempotency-Key': attempt.idempotencyKey },
                body: attempt.bodyJson
            });
        } catch (error) {
            return this.needsReconciliation(
                callerScope, businessJobId, attempt, claimToken, 'transport'
            );
        }

        if (response.statusCode >= 500) {
            const outcome = await this.needsReconciliation(
                callerScope,
                businessJobId,
                attempt,
                claimToken,
                'http_5xx',
                { httpStatus: response.statusCode }
            );
            response.body.destroy();
            return outcome;
        }

        let payload;
        try {
            payload = await SMSCodeClient._decodeJson(response);
        } catch (error) {
            return this.needsReconciliation(
                callerScope,
                businessJobId,
                attempt,
                claimToken,
                'malformed_response',
                { httpStatus: response.statusCode }
            );
        }

        if (isObject(payload) && payload.success === false) {
            const candidateCode = isObject(payload.error) ? payload.error.code : null;
            const code = typeof candidateCode === 'string' ? candidateCode : null;
            if (!isV1ErrorResponse(payload)) {
                return this.needsReconciliation(
                    callerScope,
                    businessJobId,
                    attempt,
                    claimToken,
                    'malformed_response',
                    { httpStatus: response.statusCode, errorCode: code }
                );
            }
            if (response.statusCode === 422 && DEFINITIVE_CREATE_ERRORS.has(code)) {
                attempt = await this.appendCreateEvent(
                    callerScope,
                    businessJobId,
                    attempt,
                    claimToken,
                    {
                        state: 'definitive_rejection',
                        httpStatus: response.statusCode,
                        errorCode: code
                    }
                );
                return { kind: 'definitive_rejection', attempt, code };
            }
            if (response.statusCode === 409 && code === 'INSUFFICIENT_BALANCE') {
                attempt = await this.appendCreateEvent(
                    callerScope,
                    businessJobId,
                    attempt,
                    claimToken,
                    {
                        state: 'insufficient_balance',
                        httpStatus: response.statusCode,
                        errorCode: code
                    }
                );
                return { kind: 'insufficient_balance', attempt };
            }
            return this.needsReconciliation(
                callerScope,
                businessJobId,
                attempt,
                claimToken,
                response.statusCode === 409 && code === 'REQUEST_IN_PROGRESS'
                    ? 'request_in_progress'
                    : 'unknown_response',
                { httpStatus: response.statusCode, errorCode: code }
            );
        }

        if (response.statusCode !== 200 || !isObject(payload) ||
            payload.success !== true || !('data' in payload) || 'error' in payload) {
            return this.needsReconciliation(
                callerScope,
                businessJobId,
                attempt,
                claimToken,
                'contradictory_response',
                { httpStatus: response.statusCode }
            );
        }

        let validatedResult;
        try {
            validatedResult = validateV1CreateOrderResult(payload.data);
            const quantity = JSON.parse(attempt.bodyJson).quantity;
            if (validatedResult.orders.length + validatedResult.failed_count !== quantity) {
                throw new Error('quantity conservation violated');
            }
        } catch (error) {
            return this.needsReconciliation(
                callerScope,
                businessJobId,
                attempt,
                claimToken,
                'malformed_success',
                { httpStatus: response.statusCode }
            );
        }

        attempt = await this.appendCreateEvent(
            callerScope,
            businessJobId,
            attempt,
            claimToken,
            { state: 'resolved', validatedResponse: validatedResult }
        );
        return { kind: 'resolved', attempt, result: validatedResult };
    }

    async reconcileCreateOrder(
        callerScope,
        businessJobId,
        idempotencyKey,
        reviewThresholdAt
    ) {
        const attempt = await this.attemptStore.load(callerScope, businessJobId);
        this.assertAttemptBinding(
            attempt, callerScope, businessJobId, idempotencyKey, reviewThresholdAt
        );
        return this.createOrder(
            callerScope,
            businessJobId,
            idempotencyKey,
            reviewThresholdAt,
            attempt
        );
    }

    async getOrder(orderId) {
        return this._request('GET', `/orders/${orderId}`);
    }

    async reconcileCancellation(orderId) {
        try {
            const latest = await this.getOrder(orderId);
            if (validOrderSnapshot(latest, orderId) && latest.status === 'CANCELED') {
                return {
                    kind: 'confirmed_canceled',
                    snapshot: { id: orderId, status: 'CANCELED' }
                };
            }
            return {
                kind: 'ambiguous',
                orderId,
                latestStatus: validOrderSnapshot(latest, orderId) ? latest.status : null
            };
        } catch (error) {
            return { kind: 'ambiguous', orderId, latestStatus: null };
        }
    }

    async cancelOrder(orderId) {
        let current;
        try {
            current = await this.getOrder(orderId);
        } catch (error) {
            return { kind: 'ambiguous', orderId, latestStatus: null };
        }
        if (!validOrderSnapshot(current, orderId) || typeof current.can_cancel !== 'boolean') {
            return { kind: 'ambiguous', orderId, latestStatus: null };
        }
        if (!current.can_cancel) return { kind: 'skipped', orderId };

        let response;
        try {
            response = await this._rawRequest('POST', '/orders/cancel', {
                data: { id: orderId }
            });
        } catch (error) {
            return this.reconcileCancellation(orderId);
        }

        let payload;
        try {
            payload = await SMSCodeClient._decodeJson(response);
        } catch (error) {
            return this.reconcileCancellation(orderId);
        }

        if (response.statusCode === 200) {
            const receipt = validatedCancelReceipt(payload, orderId);
            return receipt
                ? { kind: 'receipt', receipt }
                : this.reconcileCancellation(orderId);
        }
        const apiError = documentedCancelError(response.statusCode, payload);
        if (apiError) {
            return {
                kind: 'rejected',
                httpStatus: response.statusCode,
                errorCode: apiError.code
            };
        }
        return this.reconcileCancellation(orderId);
    }

    async waitForOTP(orderId, { timeoutMs = 240000, pollIntervalMs = 10000 } = {}) {
        if (!Number.isFinite(timeoutMs)) {
            throw new TypeError('timeoutMs must be finite');
        }
        // A non-positive or non-finite interval would hot-loop GETs for the
        // whole budget. Clamp to a documented minimum.
        const interval = Number.isFinite(pollIntervalMs)
            ? Math.max(1000, pollIntervalMs)
            : 10000;
        const deadline = Date.now() + timeoutMs;
        let lastSeenRevision = -1;

        // The first poll always runs, even for a non-positive budget: you asked to
        // wait zero milliseconds, not to skip looking. `timeoutMs` bounds when this
        // loop stops STARTING later polls; a request already in flight still runs
        // to its own transport bound.
        while (true) {
            const order = await this.getOrder(orderId);

            const revision = order.sms_revision;
            const message = order.otp_message;
            if (
                Number.isInteger(revision) &&
                revision > lastSeenRevision &&
                typeof message === 'string' &&
                message.trim()
            ) {
                lastSeenRevision = revision;
                console.log(`SMS revision ${revision}`);
                return message;
            }
            if (['COMPLETED', 'EXPIRED', 'CANCELED'].includes(order.status)) return null;

            // Clamp the wait to what remains, then gate: no later poll starts
            // once the budget is spent, and the cancellation path stays reachable.
            await new Promise(resolve =>
                setTimeout(resolve, Math.min(interval, Math.max(0, deadline - Date.now())))
            );
            if (Date.now() >= deadline) break;
        }

        const cancellation = await this.cancelOrder(orderId);
        if (cancellation?.kind === 'receipt') {
            console.log(`Refund Rp ${cancellation.receipt.refund_amount}; balance Rp ${cancellation.receipt.new_balance}`);
        } else if (cancellation.kind === 'confirmed_canceled') {
            console.log('Latest snapshot CANCELED है; cancel receipt या refund प्रमाण उपलब्ध नहीं है');
        } else if (cancellation.kind === 'rejected') {
            console.log(`Cancellation rejected: ${cancellation.errorCode}`);
        } else if (cancellation.kind === 'ambiguous') {
            console.log('Cancellation unresolved है; durable reconciliation में retained रखें');
        } else {
            console.log('Latest snapshot में can_cancel=false था; cancel POST नहीं भेजा');
        }
        return null;
    }
}

// इस्तेमाल का उदाहरण
async function main() {
    const client = new SMSCodeClient(
        process.env.SMSCODE_API_TOKEN,
        durableCreateAttemptStore
    );
    const callerScope = process.env.SMSCODE_CALLER_SCOPE;
    const businessJobId = process.env.SMSCODE_BUSINESS_JOB_ID;
    const idempotencyKey = process.env.SMSCODE_IDEMPOTENCY_KEY;
    const reviewThresholdAt = Number(process.env.SMSCODE_REVIEW_THRESHOLD_AT);
    const attempt = await client.prepareCreateAttempt(
        callerScope,
        businessJobId,
        idempotencyKey,
        reviewThresholdAt,
        88
    );
    const createOutcome = await client.createOrder(
        callerScope,
        businessJobId,
        idempotencyKey,
        reviewThresholdAt,
        attempt
    );
    if (createOutcome.kind !== 'resolved') {
        console.log(`Create durable state: ${createOutcome.kind}`);
        return;
    }
    if (createOutcome.result.orders.length !== 1 || createOutcome.result.failed_count !== 0) {
        throw new Error('Create resolved हुआ, लेकिन ठीक एक order नहीं मिला');
    }
    const order = createOutcome.result.orders[0];
    const orderId = order.id;
    const status = order.status;
    let assignmentStatus = status;
    let phoneNumber = order.phone_number;
    if (!(typeof phoneNumber === 'string' && phoneNumber.trim())) {
        let currentOrder = null;
        try {
            currentOrder = await client.getOrder(orderId);
        } catch {
            currentOrder = null;
        }
        const currentSnapshotIsValid = validOrderSnapshot(currentOrder, orderId);
        if (currentSnapshotIsValid) {
            assignmentStatus = currentOrder.status;
        }
        const currentPhone = currentOrder?.phone_number;
        if (
            !currentSnapshotIsValid ||
            !(typeof currentPhone === 'string' && currentPhone.trim())
        ) {
            return {
                kind: 'pending_assignment',
                create_outcome: 'resolved',
                order_id: orderId,
                status: assignmentStatus
            };
        }
        phoneNumber = currentPhone;
    }

    console.log(`वर्चुअल नंबर: ${phoneNumber}`);

    const otp = await client.waitForOTP(orderId);
    if (otp) {
        console.log(`OTP: ${otp}`);
    } else {
        console.log('OTP नहीं मिली');
    }
}

main()
    .then((outcome) => {
        if (outcome?.kind === 'pending_assignment') {
            console.log(
                `pending_assignment: order ${outcome.order_id} को अभी नंबर नहीं मिला`
            );
        }
    })
    .catch(console.error);

PHP — सरल लागूकरण

<?php
interface CreateAttemptStore {
    public function insertOrLoad(
        string $callerScope,
        string $businessJobId,
        string $requestFingerprint,
        array $candidate
    ): array;
    public function load(string $callerScope, string $businessJobId): array;
    public function claimForSend(
        string $callerScope,
        string $businessJobId,
        int $expectedVersion,
        int $leaseSeconds
    ): array;
    public function appendEvent(
        string $callerScope,
        string $businessJobId,
        string $claimToken,
        int $expectedVersion,
        array $event
    ): array;
    public function retainOperatorReview(
        string $callerScope,
        string $businessJobId,
        array $attempt,
        int $storeNow
    ): array;
    public function now(): int;
}

class SMSCodeClient {
    private const DEFINITIVE_CREATE_ERRORS = [
        'NO_OFFER_AVAILABLE',
        'VALIDATION_ERROR',
        'PROVIDER_ERROR',
        'IDEMPOTENCY_KEY_REUSED',
    ];
    private const ORDER_STATUSES = [
        'ACTIVE', 'OTP_RECEIVED', 'COMPLETED', 'CANCELED', 'EXPIRED',
    ];
    private const V1_CREATE_REQUIRED_FIELDS = [
        'id', 'status', 'product_id', 'amount', 'otp_code', 'can_finish',
        'can_resend', 'can_cancel', 'can_replace', 'can_reactivate',
        'resend_available_at', 'cancel_available_at', 'replace_available_at',
    ];
    private const V1_CREATE_FIELDS = [
        'id', 'status', 'product_id', 'amount', 'otp_code', 'can_finish',
        'can_resend', 'can_cancel', 'can_replace', 'can_reactivate',
        'resend_available_at', 'cancel_available_at', 'replace_available_at',
        'phone_number', 'otp_received_at', 'expires_at', 'failed_reason',
        'catalog_product_id', 'operator_id', 'operator_name',
    ];
    private const V1_CREATE_NULLABLE_STRING_FIELDS = [
        'phone_number', 'otp_code', 'otp_received_at', 'expires_at',
        'failed_reason', 'operator_name', 'resend_available_at',
        'cancel_available_at', 'replace_available_at',
    ];
    private const V1_CREATE_BOOLEAN_FIELDS = [
        'can_finish', 'can_resend', 'can_cancel', 'can_replace', 'can_reactivate',
    ];
    private const CANCEL_ERROR_CODES_BY_STATUS = [
        401 => ['UNAUTHORIZED'],
        404 => ['NOT_FOUND'],
        409 => ['CONFLICT', 'CANCEL_TOO_EARLY'],
        422 => ['PROVIDER_ERROR', 'VALIDATION_ERROR'],
        429 => ['RATE_LIMIT_EXCEEDED'],
        500 => ['INTERNAL_ERROR'],
        503 => ['SERVICE_UNAVAILABLE'],
    ];
    private const CREATE_CONNECT_TIMEOUT_SECONDS = 5;
    private const CREATE_TOTAL_TIMEOUT_SECONDS = 30;
    private const CREATE_LEASE_SECONDS = 45;
    private const RECONCILIATION_BASE_BACKOFF_SECONDS = 2;
    private const RECONCILIATION_MAX_BACKOFF_SECONDS = 60;
    private const CLIENT_REPLAY_MAX = 3600;
    private const MAX_AUTOMATIC_SENDS = 32;

    private string $apiToken;
    private string $baseUrl = 'https://api.smscode.gg/v1';
    private CreateAttemptStore $attemptStore;

    public function __construct(string $apiToken, CreateAttemptStore $attemptStore) {
        $this->apiToken = $apiToken;
        $this->attemptStore = $attemptStore;
    }

    private function sendRequest(
        string $method,
        string $endpoint,
        array|string $data = [],
        array $extraHeaders = []
    ): array {
        $ch = curl_init();
        $url = $this->baseUrl . $endpoint;

        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => self::CREATE_CONNECT_TIMEOUT_SECONDS,
            CURLOPT_TIMEOUT => self::CREATE_TOTAL_TIMEOUT_SECONDS,
            CURLOPT_HTTPHEADER => array_merge([
                'Authorization: Bearer ' . $this->apiToken,
                'Content-Type: application/json'
            ], $extraHeaders)
        ]);

        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            if ($data !== [] && $data !== '') {
                $payload = is_string($data)
                    ? $data
                    : json_encode($data, JSON_THROW_ON_ERROR);
                curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
            }
        }

        $rawResponse = curl_exec($ch);
        if ($rawResponse === false) {
            $message = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException($message);
        }
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        return [$httpCode, $rawResponse];
    }

    private function request(
        string $method,
        string $endpoint,
        array|string $data = [],
        array $extraHeaders = []
    ): array {
        [$httpStatus, $rawResponse] = $this->sendRequest(
            $method,
            $endpoint,
            $data,
            $extraHeaders
        );
        $response = json_decode($rawResponse, true, 512, JSON_THROW_ON_ERROR);
        if ($httpStatus !== 200 || !is_array($response) ||
            ($response['success'] ?? null) !== true ||
            !array_key_exists('data', $response) || array_key_exists('error', $response)) {
            $error = is_array($response) && is_array($response['error'] ?? null)
                ? $response['error']
                : [];
            throw new RuntimeException(
                'API Error ' . ($error['code'] ?? 'unknown') . ': ' .
                ($error['message'] ?? 'unknown')
            );
        }
        return $response['data'];
    }

    private static function isInt32(mixed $value): bool {
        return is_int($value) && $value >= -(2 ** 31) && $value <= (2 ** 31) - 1;
    }

    private static function exactFields(array $value, array $expected): bool {
        $actual = array_keys($value);
        sort($actual);
        sort($expected);
        return $actual === $expected;
    }

    private static function isV1ErrorResponse(mixed $value, mixed $shape): bool {
        if (!is_array($value) || !self::exactFields($value, ['success', 'error']) ||
            ($value['success'] ?? null) !== false || !is_array($value['error']) ||
            !($shape instanceof \stdClass) ||
            !(($shape->error ?? null) instanceof \stdClass)) {
            return false;
        }
        $error = $value['error'];
        $fields = array_keys($error);
        sort($fields);
        if ($fields !== ['code', 'message'] && $fields !== ['code', 'details', 'message']) {
            return false;
        }
        if (!is_string($error['code']) || !is_string($error['message'])) {
            return false;
        }
        if (!array_key_exists('details', $error)) {
            return true;
        }
        return property_exists($shape->error, 'details') &&
            $shape->error->details instanceof \stdClass;
    }

    private static function validateV1CreateOrderResult(mixed $value, mixed $shape): array {
        if (!is_array($value) || !self::exactFields($value, ['orders', 'failed_count'])) {
            throw new UnexpectedValueException('V1CreateOrderResult fields invalid हैं');
        }
        if (!($shape instanceof \stdClass) || !is_array($shape->orders ?? null)) {
            throw new UnexpectedValueException('orders JSON array होना चाहिए');
        }
        if (!is_array($value['orders']) || !array_is_list($value['orders'])) {
            throw new UnexpectedValueException('orders array होना चाहिए');
        }
        if (!self::isInt32($value['failed_count']) || $value['failed_count'] < 0) {
            throw new UnexpectedValueException('failed_count non-negative int32 होना चाहिए');
        }
        foreach ($value['orders'] as $item) {
            if (!is_array($item) || array_is_list($item)) {
                throw new UnexpectedValueException('हर order object होना चाहिए');
            }
            $fields = array_keys($item);
            if (array_diff(self::V1_CREATE_REQUIRED_FIELDS, $fields) !== [] ||
                array_diff($fields, self::V1_CREATE_FIELDS) !== []) {
                throw new UnexpectedValueException('V1CreateOrderItem fields invalid हैं');
            }
            if (!self::isInt32($item['id']) || !self::isInt32($item['product_id'])) {
                throw new UnexpectedValueException('order और product IDs int32 होने चाहिए');
            }
            foreach (['catalog_product_id', 'operator_id'] as $field) {
                if (array_key_exists($field, $item) && $item[$field] !== null &&
                    !self::isInt32($item[$field])) {
                    throw new UnexpectedValueException("{$field} nullable int32 होना चाहिए");
                }
            }
            if (!in_array($item['status'], self::ORDER_STATUSES, true)) {
                throw new UnexpectedValueException('unknown order status');
            }
            if (!is_int($item['amount'])) {
                throw new UnexpectedValueException('amount int64 होना चाहिए');
            }
            foreach (self::V1_CREATE_BOOLEAN_FIELDS as $field) {
                if (!is_bool($item[$field])) {
                    throw new UnexpectedValueException("{$field} boolean होना चाहिए");
                }
            }
            foreach (self::V1_CREATE_NULLABLE_STRING_FIELDS as $field) {
                if (array_key_exists($field, $item) && $item[$field] !== null &&
                    !is_string($item[$field])) {
                    throw new UnexpectedValueException("{$field} nullable string होना चाहिए");
                }
            }
        }
        return $value;
    }

    public static function validOrderSnapshot(mixed $value, int $orderId): bool {
        return is_array($value) && self::isInt32($value['id'] ?? null) &&
            $value['id'] === $orderId &&
            in_array($value['status'] ?? null, self::ORDER_STATUSES, true) &&
            !array_key_exists('refund_amount', $value) &&
            !array_key_exists('new_balance', $value);
    }

    private static function validatedCancelReceipt(mixed $payload, int $orderId): ?array {
        if (!is_array($payload) || ($payload['success'] ?? null) !== true ||
            array_diff(array_keys($payload), ['success', 'data', 'meta']) !== []) {
            return null;
        }
        $data = $payload['data'] ?? null;
        if (!is_array($data) || !self::exactFields(
            $data,
            ['order_id', 'status', 'refund_amount', 'new_balance']
        )) return null;
        if (!self::isInt32($data['order_id']) || $data['order_id'] !== $orderId ||
            $data['status'] !== 'CANCELED') return null;
        if (!is_int($data['refund_amount']) || $data['refund_amount'] < 0 ||
            !is_int($data['new_balance'])) return null;
        return $data;
    }

    private static function documentedCancelError(int $httpStatus, mixed $payload): ?array {
        if (!is_array($payload) || !self::exactFields($payload, ['success', 'error']) ||
            $payload['success'] !== false || !is_array($payload['error'])) return null;
        $error = $payload['error'];
        $fields = array_keys($error);
        if (array_diff(['code', 'message'], $fields) !== [] ||
            array_diff($fields, ['code', 'message', 'details']) !== [] ||
            !is_string($error['code']) || !is_string($error['message']) ||
            (array_key_exists('details', $error) && !is_array($error['details']))) return null;
        return in_array(
            $error['code'],
            self::CANCEL_ERROR_CODES_BY_STATUS[$httpStatus] ?? [],
            true
        ) ? $error : null;
    }

    public function prepareCreateAttempt(
        string $callerScope,
        string $businessJobId,
        string $idempotencyKey,
        int $reviewThresholdAt,
        int $catalogProductId,
        int $quantity = 1
    ): array {
        if ($callerScope === '' || $businessJobId === '' || $idempotencyKey === '') {
            throw new InvalidArgumentException(
                'Caller identity और persisted idempotency key अनिवार्य हैं'
            );
        }
        if (!self::isInt32($catalogProductId) || !self::isInt32($quantity) || $quantity <= 0) {
            throw new InvalidArgumentException('catalog_product_id int32 होना चाहिए');
        }
        $body = ['catalog_product_id' => $catalogProductId, 'quantity' => $quantity];
        ksort($body);
        $endpoint = '/orders/create';
        $bodyJson = json_encode($body, JSON_THROW_ON_ERROR);
        $requestFingerprint = hash('sha256', $endpoint . "\0" . $bodyJson);
        $startedAt = $this->attemptStore->now();
        $candidate = [
            'caller_scope' => $callerScope,
            'business_job_id' => $businessJobId,
            'endpoint' => $endpoint,
            'body_json' => $bodyJson,
            'request_fingerprint' => $requestFingerprint,
            'idempotency_key' => $idempotencyKey,
            'review_threshold_at' => $reviewThresholdAt,
            'started_at' => $startedAt,
            'effective_replay_deadline_at' => min($reviewThresholdAt, $startedAt + self::CLIENT_REPLAY_MAX),
            'sends' => 0,
            'attempt_history' => [],
        ];
        // Existing pair पर fingerprint, key या threshold अलग हो तो conflict करें।
        $attempt = $this->attemptStore->insertOrLoad(
            $callerScope,
            $businessJobId,
            $requestFingerprint,
            $candidate
        );
        $this->assertAttemptBinding(
            $attempt,
            $callerScope,
            $businessJobId,
            $idempotencyKey,
            $reviewThresholdAt
        );
        return $attempt;
    }

    private function assertAttemptBinding(
        array $attempt,
        string $callerScope,
        string $businessJobId,
        string $idempotencyKey,
        int $reviewThresholdAt
    ): void {
        $fingerprint = hash(
            'sha256',
            $attempt['endpoint'] . "\0" . $attempt['body_json']
        );
        if (!is_int($attempt['started_at'] ?? null) ||
            ($attempt['effective_replay_deadline_at'] ?? null) !== min(
                $reviewThresholdAt, $attempt['started_at'] + self::CLIENT_REPLAY_MAX
            ) || !is_int($attempt['sends'] ?? null) || $attempt['sends'] < 0) {
            throw new InvalidArgumentException('Stored replay bounds invalid हैं');
        }
        if (($attempt['caller_scope'] ?? null) !== $callerScope ||
            ($attempt['business_job_id'] ?? null) !== $businessJobId ||
            ($attempt['idempotency_key'] ?? null) !== $idempotencyKey ||
            ($attempt['review_threshold_at'] ?? null) !== $reviewThresholdAt ||
            ($attempt['endpoint'] ?? null) !== '/orders/create' ||
            ($attempt['request_fingerprint'] ?? null) !== $fingerprint) {
            throw new InvalidArgumentException(
                'Stored create attempt caller identity से मेल नहीं खाता'
            );
        }
    }

    private function appendCreateEvent(
        string $callerScope,
        string $businessJobId,
        array $attempt,
        string $claimToken,
        array $event
    ): array {
        // CAS append terminal पर lease छोड़ता है; needs_reconciliation persisted
        // next_attempt_at के साथ lease छोड़कर due queue में जाता है।
        return $this->attemptStore->appendEvent(
            $callerScope,
            $businessJobId,
            $claimToken,
            $attempt['version'],
            $event
        );
    }

    protected function sendCreateRequest(array $attempt): array {
        return $this->sendRequest(
            'POST',
            $attempt['endpoint'],
            $attempt['body_json'],
            ['Idempotency-Key: ' . $attempt['idempotency_key']]
        );
    }

    private function recoveryFields(array $attempt): array {
        return [
            'endpoint' => $attempt['endpoint'],
            'body_json' => $attempt['body_json'],
            'idempotency_key' => $attempt['idempotency_key'],
            'review_threshold_at' => $attempt['review_threshold_at'],
            'effective_replay_deadline_at' => $attempt['effective_replay_deadline_at'],
            'sends' => $attempt['sends'] ?? 0,
        ];
    }

    private function backoffSeconds(array $attempt): int {
        $sendCount = $attempt['sends'] ?? 0;
        $exponent = min(max($sendCount - 1, 0), 5);
        return min(
            self::RECONCILIATION_BASE_BACKOFF_SECONDS * (2 ** $exponent),
            self::RECONCILIATION_MAX_BACKOFF_SECONDS
        );
    }

    private function needsReconciliation(
        string $callerScope,
        string $businessJobId,
        array $attempt,
        string $claimToken,
        string $reason,
        array $evidence = []
    ): array {
        $event = array_merge([
            'state' => 'needs_reconciliation',
            'reason' => $reason,
        ], $this->recoveryFields($attempt), $evidence);
        $event['next_attempt_at'] = min(
            $this->attemptStore->now() + $this->backoffSeconds($attempt),
            $attempt['effective_replay_deadline_at']
        );
        $attempt = $this->appendCreateEvent(
            $callerScope,
            $businessJobId,
            $attempt,
            $claimToken,
            $event
        );
        return ['kind' => 'needs_reconciliation', 'attempt' => $attempt];
    }

    public function createOrder(
        string $callerScope,
        string $businessJobId,
        string $idempotencyKey,
        int $reviewThresholdAt,
        array $attempt
    ): array {
        $this->assertAttemptBinding(
            $attempt,
            $callerScope,
            $businessJobId,
            $idempotencyKey,
            $reviewThresholdAt
        );
        $preNow = $this->attemptStore->now();
        if ($preNow >= $attempt['effective_replay_deadline_at'] || ($attempt['sends'] ?? 0) >= self::MAX_AUTOMATIC_SENDS) {
            return ['kind' => 'operator_review', 'attempt' => $this->attemptStore->retainOperatorReview($callerScope, $businessJobId, $attempt, $preNow)];
        }
        $claimed = $this->attemptStore->claimForSend(
            $callerScope,
            $businessJobId,
            $attempt['version'],
            self::CREATE_LEASE_SECONDS
        );
        $attempt = $claimed['attempt'];
        $claimToken = $claimed['claim_token'];
        $this->assertAttemptBinding(
            $attempt,
            $callerScope,
            $businessJobId,
            $idempotencyKey,
            $reviewThresholdAt
        );

        $storeNow = $this->attemptStore->now();
        if ($storeNow >= $attempt['effective_replay_deadline_at'] || ($attempt['sends'] ?? 0) >= self::MAX_AUTOMATIC_SENDS) {
            $attempt = $this->appendCreateEvent(
                $callerScope,
                $businessJobId,
                $attempt,
                $claimToken,
                array_merge([
                    'state' => 'operator_review',
                    'retained_at' => $storeNow,
                ], $this->recoveryFields($attempt))
            );
            return ['kind' => 'operator_review', 'attempt' => $attempt];
        }

        $sendNumber = ($attempt['sends'] ?? 0) + 1;
        $attempt = $this->appendCreateEvent(
            $callerScope,
            $businessJobId,
            $attempt,
            $claimToken,
            array_merge($this->recoveryFields($attempt), [
                'number' => $sendNumber,
                'state' => 'sending',
                'last_send_at' => $storeNow,
                'deadline_at' => $storeNow + self::CREATE_TOTAL_TIMEOUT_SECONDS,
                'sends' => $sendNumber,
            ])
        );

        try {
            [$httpStatus, $rawBody] = $this->sendCreateRequest($attempt);
        } catch (\Throwable $error) {
            return $this->needsReconciliation(
                $callerScope, $businessJobId, $attempt, $claimToken, 'transport'
            );
        }

        if ($httpStatus >= 500) {
            return $this->needsReconciliation(
                $callerScope,
                $businessJobId,
                $attempt,
                $claimToken,
                'http_5xx',
                ['http_status' => $httpStatus]
            );
        }

        try {
            $payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
            $payloadShape = json_decode($rawBody, false, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException $error) {
            return $this->needsReconciliation(
                $callerScope,
                $businessJobId,
                $attempt,
                $claimToken,
                'malformed_response',
                ['http_status' => $httpStatus]
            );
        }

        if (is_array($payload) && ($payload['success'] ?? null) === false) {
            $error = $payload['error'] ?? null;
            $code = is_array($error) ? ($error['code'] ?? null) : null;
            $code = is_string($code) ? $code : null;
            if (!self::isV1ErrorResponse($payload, $payloadShape)) {
                return $this->needsReconciliation(
                    $callerScope,
                    $businessJobId,
                    $attempt,
                    $claimToken,
                    'malformed_response',
                    ['http_status' => $httpStatus, 'error_code' => $code]
                );
            }
            if ($httpStatus === 422 && in_array($code, self::DEFINITIVE_CREATE_ERRORS, true)) {
                $attempt = $this->appendCreateEvent(
                    $callerScope,
                    $businessJobId,
                    $attempt,
                    $claimToken,
                    [
                        'state' => 'definitive_rejection',
                        'http_status' => $httpStatus,
                        'error_code' => $code,
                    ]
                );
                return ['kind' => 'definitive_rejection', 'attempt' => $attempt, 'code' => $code];
            }
            if ($httpStatus === 409 && $code === 'INSUFFICIENT_BALANCE') {
                $attempt = $this->appendCreateEvent(
                    $callerScope,
                    $businessJobId,
                    $attempt,
                    $claimToken,
                    [
                        'state' => 'insufficient_balance',
                        'http_status' => $httpStatus,
                        'error_code' => $code,
                    ]
                );
                return ['kind' => 'insufficient_balance', 'attempt' => $attempt];
            }
            return $this->needsReconciliation(
                $callerScope,
                $businessJobId,
                $attempt,
                $claimToken,
                $httpStatus === 409 && $code === 'REQUEST_IN_PROGRESS'
                    ? 'request_in_progress'
                    : 'unknown_response',
                ['http_status' => $httpStatus, 'error_code' => $code]
            );
        }

        if ($httpStatus !== 200 || !is_array($payload) ||
            ($payload['success'] ?? null) !== true || !array_key_exists('data', $payload) ||
            array_key_exists('error', $payload)) {
            return $this->needsReconciliation(
                $callerScope,
                $businessJobId,
                $attempt,
                $claimToken,
                'contradictory_response',
                ['http_status' => $httpStatus]
            );
        }

        try {
            $validatedResult = self::validateV1CreateOrderResult(
                $payload['data'],
                $payloadShape instanceof \stdClass ? ($payloadShape->data ?? null) : null
            );
            $quantity = json_decode($attempt['body_json'], true, 512, JSON_THROW_ON_ERROR)['quantity'];
            if (count($validatedResult['orders']) + $validatedResult['failed_count'] !== $quantity) {
                throw new UnexpectedValueException('quantity conservation violated');
            }
        } catch (UnexpectedValueException $error) {
            return $this->needsReconciliation(
                $callerScope,
                $businessJobId,
                $attempt,
                $claimToken,
                'malformed_success',
                ['http_status' => $httpStatus]
            );
        }

        $attempt = $this->appendCreateEvent(
            $callerScope,
            $businessJobId,
            $attempt,
            $claimToken,
            ['state' => 'resolved', 'validated_response' => $validatedResult]
        );
        return [
            'kind' => 'resolved',
            'attempt' => $attempt,
            'result' => $validatedResult,
        ];
    }

    public function reconcileCreateOrder(
        string $callerScope,
        string $businessJobId,
        string $idempotencyKey,
        int $reviewThresholdAt
    ): array {
        $attempt = $this->attemptStore->load($callerScope, $businessJobId);
        $this->assertAttemptBinding(
            $attempt,
            $callerScope,
            $businessJobId,
            $idempotencyKey,
            $reviewThresholdAt
        );
        return $this->createOrder(
            $callerScope,
            $businessJobId,
            $idempotencyKey,
            $reviewThresholdAt,
            $attempt
        );
    }

    public function getOrder(int $orderId): array {
        return $this->request('GET', "/orders/{$orderId}");
    }

    private function reconcileCancellation(int $orderId): array {
        try {
            $latest = $this->getOrder($orderId);
        } catch (Throwable $error) {
            return ['kind' => 'ambiguous', 'order_id' => $orderId, 'latest_status' => null];
        }
        if (self::validOrderSnapshot($latest, $orderId) &&
            $latest['status'] === 'CANCELED') {
            return [
                'kind' => 'confirmed_canceled',
                'snapshot' => ['id' => $orderId, 'status' => 'CANCELED'],
            ];
        }
        return [
            'kind' => 'ambiguous',
            'order_id' => $orderId,
            'latest_status' => self::validOrderSnapshot($latest, $orderId)
                ? $latest['status']
                : null,
        ];
    }

    public function cancelOrder(int $orderId): array {
        try {
            $current = $this->getOrder($orderId);
        } catch (Throwable $error) {
            return ['kind' => 'ambiguous', 'order_id' => $orderId, 'latest_status' => null];
        }
        if (!self::validOrderSnapshot($current, $orderId) ||
            !is_bool($current['can_cancel'] ?? null)) {
            return ['kind' => 'ambiguous', 'order_id' => $orderId, 'latest_status' => null];
        }
        if (!$current['can_cancel']) return ['kind' => 'skipped', 'order_id' => $orderId];

        try {
            [$httpStatus, $rawBody] = $this->sendRequest(
                'POST',
                '/orders/cancel',
                ['id' => $orderId]
            );
        } catch (Throwable $error) {
            return $this->reconcileCancellation($orderId);
        }
        try {
            $payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException $error) {
            return $this->reconcileCancellation($orderId);
        }

        if ($httpStatus === 200) {
            $receipt = self::validatedCancelReceipt($payload, $orderId);
            return $receipt !== null
                ? ['kind' => 'receipt', 'receipt' => $receipt]
                : $this->reconcileCancellation($orderId);
        }
        $apiError = self::documentedCancelError($httpStatus, $payload);
        if ($apiError !== null) {
            return [
                'kind' => 'rejected',
                'http_status' => $httpStatus,
                'error_code' => $apiError['code'],
            ];
        }
        return $this->reconcileCancellation($orderId);
    }

    public function waitForOTP(int $orderId, int $timeoutSeconds = 240): ?string {
        $deadline = time() + $timeoutSeconds;
        $lastSeenRevision = -1;

        // The first poll always runs, even for a non-positive budget.
        // `$timeoutSeconds` bounds when this loop stops STARTING later polls.
        while (true) {
            $order = $this->getOrder($orderId);

            $smsRevision = $order['sms_revision'] ?? null;
            $otpMessage = $order['otp_message'] ?? null;
            if (is_int($smsRevision) &&
                $smsRevision > $lastSeenRevision &&
                is_string($otpMessage) &&
                trim($otpMessage) !== '') {
                $lastSeenRevision = $smsRevision;
                return $otpMessage;
            }
            if (in_array($order['status'], ['COMPLETED', 'EXPIRED', 'CANCELED'])) return null;

            // Clamp to the remaining budget, then gate: no later poll starts once
            // it is spent, and the cancellation path below stays reachable.
            sleep((int) max(0, min(10, $deadline - time())));
            if (time() >= $deadline) {
                break;
            }
        }

        $cancellation = $this->cancelOrder($orderId);
        if ($cancellation['kind'] === 'receipt') {
            $receipt = $cancellation['receipt'];
            echo "Refund Rp {$receipt['refund_amount']}; balance Rp {$receipt['new_balance']}\n";
        } elseif ($cancellation['kind'] === 'confirmed_canceled') {
            echo "Latest snapshot CANCELED है; cancel receipt या refund प्रमाण उपलब्ध नहीं है\n";
        } elseif ($cancellation['kind'] === 'rejected') {
            echo "Cancellation rejected: {$cancellation['error_code']}\n";
        } elseif ($cancellation['kind'] === 'ambiguous') {
            echo "Cancellation unresolved है; durable reconciliation में retained रखें\n";
        } else {
            echo "Latest snapshot में can_cancel=false था; cancel POST नहीं भेजा\n";
        }
        return null;
    }
}

// इस्तेमाल का उदाहरण
$attemptStore = require __DIR__ . '/durable-create-attempt-store.php';
$client = new SMSCodeClient(getenv('SMSCODE_API_TOKEN'), $attemptStore);
$callerScope = getenv('SMSCODE_CALLER_SCOPE');
$businessJobId = getenv('SMSCODE_BUSINESS_JOB_ID');
$idempotencyKey = getenv('SMSCODE_IDEMPOTENCY_KEY');
$reviewThresholdAt = (int) getenv('SMSCODE_REVIEW_THRESHOLD_AT');
$attempt = $client->prepareCreateAttempt(
    $callerScope,
    $businessJobId,
    $idempotencyKey,
    $reviewThresholdAt,
    88
);
$createOutcome = $client->createOrder(
    $callerScope,
    $businessJobId,
    $idempotencyKey,
    $reviewThresholdAt,
    $attempt
);
if ($createOutcome['kind'] !== 'resolved') {
    echo "Create durable state: {$createOutcome['kind']}\n";
    return;
}
if (count($createOutcome['result']['orders']) !== 1 ||
    $createOutcome['result']['failed_count'] !== 0) {
    throw new RuntimeException('Create resolved हुआ, लेकिन ठीक एक order नहीं मिला');
}
$order = $createOutcome['result']['orders'][0];
$orderId = $order['id'];
$status = $order['status'];
$assignmentStatus = $status;
$phoneNumber = $order['phone_number'] ?? null;
if (!(is_string($phoneNumber) && trim($phoneNumber) !== '')) {
    try {
        $currentOrder = $client->getOrder($orderId);
    } catch (Throwable $error) {
        $currentOrder = null;
    }
    $currentSnapshotIsValid = SMSCodeClient::validOrderSnapshot($currentOrder, $orderId);
    if ($currentSnapshotIsValid) {
        $assignmentStatus = $currentOrder['status'];
    }
    $currentPhone = is_array($currentOrder) ? ($currentOrder['phone_number'] ?? null) : null;
    if (!$currentSnapshotIsValid ||
        !is_string($currentPhone) ||
        trim($currentPhone) === '') {
        return [
            'kind' => 'pending_assignment',
            'create_outcome' => 'resolved',
            'order_id' => $orderId,
            'status' => $assignmentStatus,
        ];
    }
    $phoneNumber = $currentPhone;
}

echo "वर्चुअल नंबर: {$phoneNumber}\n";

$otp = $client->waitForOTP($orderId);
echo $otp ? "OTP: {$otp}\n" : "OTP नहीं मिली\n";

वेबहुक — प्रोडक्शन में अनुशंसित तरीका

पोलिंग की विशेषताएँ:

  • interval और total timeout client तय करता है
  • इस guide में positive Retry-After का client-local cap 60 सेकंड है, API SLA नहीं; fallback अधिकतम 30 सेकंड है
  • हर sleep को absolute deadline में बची अवधि तक clamp करें; हर GET के connect और total transport timeout को भी उसी बची अवधि में रखें, और deadline के बाद नया GET न भेजें
  • terminal status और expires_at server से पढ़ने होते हैं
  • webhook खोने पर reconciliation दे सकता है

वेबहुक के फ़ायदे:

  • asynchronous event delivery
  • polling अनुरोध कम हो सकते हैं
  • retry के बाद duplicate delivery सम्भव है, इसलिए durable deduplication ज़रूरी है
  • polling को reconciliation path के रूप में रखा जा सकता है

वेबहुक सेटअप

  1. Account → Webhook Notifications अनुभाग पर जाएँ
  2. HTTPS एंडपॉइंट URL दर्ज करें (सार्वजनिक और HTTPS अनिवार्य)
  3. Save करें; secret अपने-आप बनेगा और Send Test से endpoint जाँच सकते हैं

वेबहुक पेलोड

OTP event के लिए signed POST delivery की जाती है और failure पर retry हो सकता है:

{
  "event": "order.otp_received",
  "timestamp": "2026-08-10T09:15:30Z",
  "data": {
    "order_id": 90210,
    "phone_number": "+919876543210",
    "otp_code": null,
    "otp_message": "Your WhatsApp code: 847291. Don't share this code.",
    "sms_revision": 1,
    "product_id": 1024,
    "catalog_product_id": 88,
    "country": "India",
    "platform": "WhatsApp",
    "operator_id": null,
    "operator_name": null,
    "can_finish": true,
    "can_resend": false,
    "can_cancel": false,
    "can_replace": false,
    "can_reactivate": false,
    "resend_available_at": null,
    "cancel_available_at": null,
    "replace_available_at": null
  }
}

वेबहुक हैंडलर (Express.js)

const express = require('express');
const crypto = require('crypto');
const { durableInbox } = require('./durable-inbox');
const app = express();

function verifyWebhookSignature(rawBody, signature) {
    const expected = `sha256=${crypto
        .createHmac('sha256', process.env.SMSCODE_WEBHOOK_SECRET)
        .update(rawBody)
        .digest('hex')}`;
    const actualBuffer = Buffer.from(signature);
    const expectedBuffer = Buffer.from(expected);
    return actualBuffer.length === expectedBuffer.length &&
        crypto.timingSafeEqual(actualBuffer, expectedBuffer);
}

function normalizeWebhookEvent(event, data) {
    if (event === 'webhook.test') {
        return { event, message: data.message };
    }
    return {
        event,
        orderId: data.order_id,
        message: data.otp_message,
        revision: data.sms_revision
    };
}

function webhookDedupeKey(event, data, rawBody) {
    if (event === 'webhook.test') {
        return `${event}:${crypto.createHash('sha256').update(rawBody).digest('hex')}`;
    }
    if (event === 'order.otp_received') {
        return `${event}:${data.order_id}:${data.sms_revision}`;
    }
    return `${event}:${data.order_id}`;
}

async function persistOrEnqueueOnce(dedupeKey, eventRecord) {
    // insertIfAbsent को unique key पर atomic होना चाहिए।
    await durableInbox.insertIfAbsent(dedupeKey, eventRecord);
}

app.post(
    '/webhook/smscode',
    express.raw({ type: 'application/json' }),
    async (req, res) => {
        const signature = req.get('X-Webhook-Signature') || '';
        if (!verifyWebhookSignature(req.body, signature)) {
            return res.status(401).json({ error: 'Invalid signature' });
        }

        const { event, data } = JSON.parse(req.body.toString('utf8'));
        const dedupeKey = webhookDedupeKey(event, data, req.body);
        const eventRecord = normalizeWebhookEvent(event, data);
        await persistOrEnqueueOnce(dedupeKey, eventRecord);
        return res.status(200).json({ received: true });
    }
);

app.listen(3000, () => console.log('वेबहुक सर्वर तैयार'));

insertIfAbsent को unique dedupe key पर atomic रखें। OTP event के लिए key में event, order_id और sms_revision; terminal event के लिए event और order_id; तथा webhook.test के लिए raw-body SHA-256 रखें। Request handler केवल event को normalize करके durable inbox में लिखता है; business side effects अलग worker में inbox से चलाएँ। Durable insert विफल हो तो 2xx न भेजें।

त्रुटि प्रबंधन — पूरी गाइड

त्रुटि कोड संदर्भ तालिका

कोड HTTP अर्थ अनुशंसित कार्रवाई
INSUFFICIENT_BALANCE 409 बैलेंस कम है एक POST के बाद रुकें और उपयोगकर्ता को सूचित करें
SERVICE_UNAVAILABLE 503 API अस्थायी रूप से अनुपलब्ध paid create को अस्पष्ट मानें और reconciliation करें
NOT_FOUND 404 अमान्य order_id order_id की वैधता जाँचें
RATE_LIMIT_EXCEEDED 429 बहुत अधिक अनुरोध बैकऑफ से पुनः प्रयास
UNAUTHORIZED 401 API टोकन अमान्य टोकन जाँचें या दोबारा बनाएँ
VALIDATION_ERROR 422 request validation विफल request body और catalog ID जाँचें
CONFLICT 409 request वर्तमान resource state से टकराता है नवीनतम snapshot पढ़ें

एक्सपोनेंशियल बैकऑफ के साथ पुनः प्रयास तर्क

import time
import random
from urllib3.util import Timeout

def poll_request_with_retry(
    url, headers, max_retries=3, base_delay=1.0, timeout_seconds=120
):
    """केवल read-only GET polling को absolute local deadline में retry करें।"""
    deadline = time.time() + max(0, timeout_seconds)

    for attempt in range(max_retries):
        remaining_seconds = deadline - time.time()
        if remaining_seconds <= 0:
            raise TimeoutError("polling deadline reached")
        request_total_seconds = min(30, remaining_seconds)
        request_connect_seconds = min(5, request_total_seconds)
        request_timeout = Timeout(
            connect=request_connect_seconds,
            total=request_total_seconds,
        )

        response = requests.get(
            url,
            headers=headers,
            timeout=request_timeout,
        )
        if response.status_code != 429:
            response.raise_for_status()
            return response
        if attempt == max_retries - 1:
            response.raise_for_status()

        raw_retry_after = response.headers.get("Retry-After", "")
        try:
            parsed_retry_after = int(raw_retry_after)
        except (TypeError, ValueError):
            parsed_retry_after = 0
        delay = (
            min(parsed_retry_after, 60)
            if parsed_retry_after > 0
            else min(base_delay * (2 ** attempt) + random.uniform(0, 1), 30)
        )
        remaining_seconds = max(0, deadline - time.time())
        if remaining_seconds <= 0:
            raise TimeoutError("polling deadline reached")
        time.sleep(min(delay, remaining_seconds))

    raise RuntimeError("bounded polling retry exhausted")

दर सीमा संभालना

Guide किसी स्थिर account या IP quota की घोषणा नहीं करता। Client-side concurrency और polling को bounded रखें। इस उदाहरण का positive Retry-After cap 60 सेकंड की local client policy है, API SLA नहीं; header absent या malformed हो तो अधिकतम 30 सेकंड का fallback लें। हर sleep को absolute deadline में बची अवधि तक clamp करें और हर GET के connect तथा total transport timeout को भी उसी बची अवधि में रखें, ताकि in-flight transport bounded रहे। Deadline के बाद नया GET न भेजें। Paid create को generic retry loop में न डालें।

सुरक्षा की सर्वोत्तम प्रथाएँ

API इंटीग्रेशन में सुरक्षा से समझौता नहीं होना चाहिए। कुछ ज़रूरी बातें:

टोकन सुरक्षा:

  • API टोकन हमेशा पर्यावरण चर में रखें, कोड में कभी नहीं
  • .gitignore में .env फ़ाइलें जोड़ें
  • compromise या access change पर वर्तमान टोकन regenerate करें
  • environment isolation के लिए अलग account या backend access boundary रखें

वेबहुक सत्यापन:

  • प्रोडक्शन में वेबहुक पेलोड का हस्ताक्षर सत्यापित करें
  • पुष्टि करें कि अनुरोध वास्तव में SMSCode से आया है
  • Account → Webhook Notifications में endpoint save करके बना secret सुरक्षित रखें
  • X-Webhook-Signature हेडर को हमेशा सत्यापित करें

नेटवर्क सुरक्षा:

  • सभी API संचार HTTPS पर होना चाहिए
  • वेबहुक एंडपॉइंट भी HTTPS पर हो
  • IP व्हाइटलिस्टिंग उपलब्ध होने पर ज़रूर करें

वर्चुअल नंबर सुरक्षित है या नहीं — यह गाइड भी पढ़ें जो आपके उपयोगकर्ताओं की चिंताओं का जवाब देती है।

एकाधिक देशों और सेवाओं की रणनीति

SMSCode API का एक बड़ा फ़ायदा यह है कि एक ही इंटीग्रेशन से आप सौ से ज़्यादा देशों और पाँच सौ से ज़्यादा सेवाओं तक पहुँच सकते हैं। लेकिन इसका सही इस्तेमाल करने के लिए कुछ रणनीतियाँ अपनानी होती हैं।

देश की प्राथमिकता कैसे तय करें:

हर सेवा के लिए सभी देशों में नंबर समान रूप से उपलब्ध नहीं होते। उदाहरण के लिए, WhatsApp के लिए भारतीय नंबर बहुत सस्ते और आसानी से उपलब्ध हैं, जबकि Telegram के लिए रूसी नंबर अधिक विश्वसनीय हो सकते हैं। Catalog एंडपॉइंट से OpenAPI का integer available फ़ील्ड देखकर विकल्प चुनें।

फॉलबैक रणनीति:

def select_catalog_product(client, country_id, platform_id):
    """Catalog से एक उपलब्ध उत्पाद चुनें; यहाँ कोई paid create नहीं होता।"""
    catalog = client.get_catalog(country_id=country_id, platform_id=platform_id)
    available = [
        item for item in catalog
        if item['available'] and type(item.get('catalog_product_id')) is int
    ]
    if not available:
        raise Exception("इस देश और platform के लिए कोई उत्पाद उपलब्ध नहीं")
    return available[0]['catalog_product_id']

def create_selected_order(
    client,
    caller_scope,
    business_job_id,
    idempotency_key,
    review_threshold_at,
    country_id,
    platform_id,
):
    catalog_product_id = select_catalog_product(client, country_id, platform_id)
    attempt = client.prepare_create_attempt(
        caller_scope,
        business_job_id,
        idempotency_key,
        review_threshold_at,
        catalog_product_id,
    )
    return client.create_order(
        caller_scope,
        business_job_id,
        idempotency_key,
        review_threshold_at,
        attempt,
    )

बैलेंस की योजना बनाना:

प्रोडक्शन में बैलेंस खत्म होने से बड़ी परेशानी हो सकती है। एक स्वचालित निगरानी प्रणाली बनाएँ जो बैलेंस एक निश्चित सीमा से नीचे जाने पर ईमेल या SMS अलर्ट भेजे। /v1/balance एंडपॉइंट को हर घंटे जाँचें और टीम को समय पर सूचित करें।

परीक्षण और विकास का माहौल

API के साथ काम करते समय स्थानीय विकास और प्रोडक्शन वातावरण को अलग रखना ज़रूरी है।

पर्यावरण प्रबंधन:

# विकास (.env.development)
SMSCODE_API_TOKEN=dev_token_here
SMSCODE_LOG_LEVEL=debug

# उत्पादन (.env.production)
SMSCODE_API_TOKEN=prod_token_here
SMSCODE_LOG_LEVEL=error

एकीकरण परीक्षण — मॉक क्लाइंट:

असली API कॉल के बिना अपने कोड को परीक्षण करने के लिए API क्लाइंट को मॉक करें। इससे टेस्टिंग तेज़ होती है और बैलेंस भी नहीं खर्च होता:

from unittest.mock import MagicMock

def test_otp_flow(
    caller_scope,
    business_job_id,
    idempotency_key,
    review_threshold_at,
):
    """मॉक API क्लाइंट से परीक्षण करें"""
    mock_client = MagicMock()

    # ऑर्डर बनाने का मॉक जवाब
    stored_attempt = {
        "caller_scope": caller_scope,
        "business_job_id": business_job_id,
        "idempotency_key": idempotency_key,
        "review_threshold_at": review_threshold_at,
    }
    mock_client.prepare_create_attempt.return_value = stored_attempt
    mock_client.create_order.return_value = {
        "kind": "resolved",
        "attempt": stored_attempt,
        "result": {
            "orders": [{
                "id": 90210,
                "status": "ACTIVE",
                "phone_number": "+919999999999",
                "product_id": 1024,
                "amount": 15000,
                "otp_code": None,
                "can_finish": False,
                "can_resend": False,
                "can_cancel": False,
                "can_replace": False,
                "can_reactivate": False,
                "resend_available_at": None,
                "cancel_available_at": None,
                "replace_available_at": None,
            }],
            "failed_count": 0,
        },
    }

    # OTP का मॉक जवाब
    mock_client.wait_for_otp.return_value = "123456"

    result = verify_whatsapp(
        mock_client,
        caller_scope,
        business_job_id,
        idempotency_key,
        review_threshold_at,
    )
    assert result["otp"] == "123456"
    assert result["phone"] == "+919999999999"
    print("परीक्षण सफल!")

इस तरह के यूनिट टेस्ट से आप असली API कॉल किए बिना अपने लॉजिक को सत्यापित कर सकते हैं — जिससे विकास की गति बढ़ती है और टेस्टिंग की लागत कम रहती है।

प्रोडक्शन चेकलिस्ट

सुरक्षा:

  • API टोकन पर्यावरण चर में (हार्डकोड नहीं)
  • .gitignore में .env फ़ाइलें
  • वेबहुक एंडपॉइंट HTTPS
  • टोकन रोटेशन अनुसूची तय करें

विश्वसनीयता:

  • paid create के लिए caller-owned identity और exact-key/body durable reconciliation
  • local timeout पर latest snapshot पढ़कर केवल can_cancel=true होने पर cancel करें
  • त्रुटि लॉगिंग (order_id + error_code + टाइमस्टैंप)
  • बैलेंस निगरानी अलर्ट (कम बैलेंस सूचना)

प्रदर्शन:

  • Catalog कैशिंग लागू करें
  • पोलिंग के बजाय वेबहुक को प्राथमिकता दें
  • एक साथ कई ऑर्डर की दर सीमित करें
  • डेटाबेस में ऑर्डर स्थिति सेव करें

अवलोकनीयता:

  • OTP डिलीवरी समय ट्रैक करें
  • प्रति सेवा सफलता दर नज़र रखें
  • विफल ऑर्डर के पैटर्न का विश्लेषण करें

वास्तविक उपयोग के मामले

उपयोग 1: उपयोगकर्ता रजिस्ट्रेशन टेस्टिंग

def create_test_accounts(client, catalog_product_id, persisted_jobs):
    """लोड टेस्टिंग के लिए कई टेस्ट अकाउंट बनाएँ"""
    accounts = []

    # हर entry पहले से durable application-job record है; loop restart ID नहीं बनाता।
    for job in persisted_jobs:
        caller_scope = job["caller_scope"]
        business_job_id = job["business_job_id"]
        idempotency_key = job["idempotency_key"]
        review_threshold_at = job["review_threshold_at"]
        attempt = client.prepare_create_attempt(
            caller_scope,
            business_job_id,
            idempotency_key,
            review_threshold_at,
            catalog_product_id,
        )
        create_outcome = client.create_order(
            caller_scope,
            business_job_id,
            idempotency_key,
            review_threshold_at,
            attempt,
        )
        if create_outcome["kind"] != "resolved":
            accounts.append({
                "create_outcome": create_outcome["kind"],
                "attempt": create_outcome["attempt"],
            })
            continue
        create_result = create_outcome["result"]
        if len(create_result["orders"]) != 1 or create_result["failed_count"] != 0:
            accounts.append({"create_outcome": "resolved_without_single_order"})
            continue
        order = create_result["orders"][0]

        # `phone_number` assignment तक optional/nullable है। उसी id पर एक bounded
        # read; दूसरा paid create कभी नहीं।
        phone = order.get("phone_number")
        if not (isinstance(phone, str) and phone.strip()):
            phone = client.get_order(order["id"]).get("phone_number")
        if not (isinstance(phone, str) and phone.strip()):
            accounts.append({
                "create_outcome": "pending_assignment",
                "order_id": order["id"],
            })
            continue

        otp = client.wait_for_otp(order["id"])

        if otp:
            accounts.append({"phone": phone, "otp": otp})

        time.sleep(2)  # केवल local pacing; यह API rate-limit contract नहीं है

    return accounts

उपयोग 2: प्राइवेसी सेवा के रूप में

अपने उपयोगकर्ताओं को गोपनीयता सुरक्षा दें:

  • उपयोगकर्ता किसी मंच के लिए वर्चुअल नंबर माँगता है
  • आपका ऐप SMSCode API को कॉल करता है
  • उपयोगकर्ता को वर्चुअल नंबर वापस मिलता है
  • वेबहुक के ज़रिए OTP उपयोगकर्ता तक पहुँचाई जाती है

वर्चुअल नंबर खरीदने की गाइड में इस बारे में विस्तार से बताया गया है।

उपयोग 3: स्वचालित QA फ्रेमवर्क

def test_registration_flow(
    client,
    app_url,
    catalog_product_id,
    caller_scope,
    business_job_id,
    idempotency_key,
    review_threshold_at,
):
    """शुरू से अंत तक रजिस्ट्रेशन टेस्ट"""
    # वर्चुअल नंबर पाएँ
    attempt = client.prepare_create_attempt(
        caller_scope,
        business_job_id,
        idempotency_key,
        review_threshold_at,
        catalog_product_id,
    )
    create_outcome = client.create_order(
        caller_scope,
        business_job_id,
        idempotency_key,
        review_threshold_at,
        attempt,
    )
    if create_outcome["kind"] != "resolved":
        return {"create_outcome": create_outcome["kind"], "attempt": create_outcome["attempt"]}
    create_result = create_outcome["result"]
    if len(create_result["orders"]) != 1 or create_result["failed_count"] != 0:
        return {"create_outcome": "resolved_without_single_order"}
    order = create_result["orders"][0]

    # `phone_number` assignment तक optional/nullable है। Registration शुरू करने से
    # पहले उसी id पर एक bounded read; दूसरा paid create कभी नहीं।
    phone = order.get("phone_number")
    if not (isinstance(phone, str) and phone.strip()):
        phone = client.get_order(order["id"]).get("phone_number")
    if not (isinstance(phone, str) and phone.strip()):
        # pending_assignment: order resolved और charged है; registration न भेजें।
        return {"create_outcome": "pending_assignment", "order_id": order["id"]}

    # अपने ऐप पर रजिस्ट्रेशन शुरू करें
    requests.post(
        f"{app_url}/register",
        json={"phone": phone},
        timeout=Timeout(connect=5, total=30),
    )

    # OTP पाएँ
    otp = client.wait_for_otp(order["id"])
    assert otp, "OTP नहीं मिली"

    # वेरिफिकेशन पूरा करें
    response = requests.post(
        f"{app_url}/verify",
        json={"phone": phone, "otp": otp},
        timeout=Timeout(connect=5, total=30),
    )
    assert response.json()["success"]
    print("रजिस्ट्रेशन प्रवाह सफलतापूर्वक पास हुआ")

SMSCode API का इस्तेमाल कैसे करें — इस विस्तृत गाइड में और भी उदाहरण मिलेंगे।

API इंटीग्रेशन में आम गलतियाँ

नए डेवलपर अक्सर कुछ गलतियाँ करते हैं जिनसे बचना ज़रूरी है:

गलती 1: बहुत ज़्यादा पोलिंग

हर एक सेकंड पर status जाँचना अनावश्यक load बनाता है। अपने workload के लिए bounded interval और absolute deadline रखें। इस guide का 60-second positive Retry-After cap local client policy है, API SLA नहीं; sleep को बची deadline तक clamp करें, हर GET के connect और total transport timeout को भी उसी बची अवधि में रखें, और webhook के साथ polling reconciliation रखें।

गलती 2: ऑर्डर रद्द न करना

अगर उपयोगकर्ता प्रक्रिया बीच में छोड़ दे, latest snapshot पढ़ें। केवल can_cancel=true होने पर cancel करें। refund_amount तथा new_balance केवल 200 success envelope के data.status=CANCELED receipt से लें; timeout के बाद मिला CANCELED GET snapshot refund receipt नहीं है।

गलती 3: Catalog कैश न करना

हर ऑर्डर से पहले पूरा Catalog फिर से लोड करना अनावश्यक हो सकता है। Workload के अनुसार bounded cache रखें, pagination संभालें और paid create से पहले stale product data refresh करें।

गलती 4: एरर हैंडलिंग न करना

नेटवर्क त्रुटियाँ होती ही हैं। बिना एरर हैंडलिंग के प्रोडक्शन में अनुप्रयोग क्रैश हो सकता है। हमेशा try-catch और retry लॉजिक रखें।

गलती 5: एक token को client-side code में रखना

API token केवल server-side secret store में रखें। एक account में एक current token होता है; environment isolation के लिए अलग account या backend access boundary इस्तेमाल करें।

FAQ

SMSCode API की मुफ्त योजना है?

API पहुँच अकाउंट से होती है और हर paid create उपलब्ध बैलेंस इस्तेमाल करता है। वर्तमान लागत के लिए catalog की पूर्णांक IDR price फ़ील्ड देखें; किसी स्थिर प्रति-देश मूल्य या निश्चित टेस्ट बजट पर निर्भर न रहें।

API से एक साथ कितने ऑर्डर दे सकते हैं?

Guide कोई स्थिर concurrent-order quota या plan घोषित नहीं करता। Current catalog stock, balance और API response को authority मानें, client-side concurrency सीमित रखें और 429 को bounded तरीके से संभालें।

वेबहुक हस्ताक्षर सत्यापन कैसे करें?

प्रोडक्शन में वेबहुक के raw bytes का हस्ताक्षर parse करने से पहले ज़रूर सत्यापित करें। Account → Webhook Notifications में endpoint save करने पर बने secret से हर X-Webhook-Signature हेडर जाँचें।

OTP औसतन कितने सेकंड में आता है?

Delivery समय platform, country और upstream response पर निर्भर करता है; इस guide में fixed latency guarantee नहीं है। हर order का server-provided expires_at पढ़ें। बिना SMS के server-side EXPIRED transition debit लौटाता है, लेकिन SMS मिलने के बाद final order refund के बिना पूरा हो सकता है।

क्या परीक्षण वातावरण उपलब्ध है?

अलग से कोई sandbox credit वातावरण नहीं है। वास्तविक API पर catalog से कम लागत वाला उपलब्ध उत्पाद चुनें, paid create की संख्या सीमित रखें, और अस्पष्ट परिणाम पर caller के persisted review threshold तक उसी key तथा उसी body से store-clock reconciliation करें; unresolved job को operator_review में retained रखें।

API टोकन खो जाए तो क्या करें?

Dashboard → Account → API अनुभाग में current token regenerate करें। Regeneration database में पुराने token को replace करती है, इसलिए सभी authorized server deployments को नए secret पर atomically update करें।

किन सेवाओं के लिए वर्चुअल नंबर मिलते हैं?

उपलब्ध service/country combinations समय के साथ बदलते हैं। Paginated GET /v1/catalog/products से current active, available और integer-IDR price पढ़ें; guide में स्थिर सूची या संख्या पर निर्भर न रहें।

SMSCode आज़माने के लिए तैयार?

अकाउंट बनाएं और दो मिनट से भी कम में अपना पहला वर्चुअल नंबर पाएं।

शुरू करें →