Ruby SDK
StampMitra Developer API का आधिकारिक Ruby क्लाइंट: typed immutable responses, अपने आप retries और Rails, Sinatra व Rack के लिए signed-webhook helpers के साथ identity verification और e-Stamp, सिर्फ़ standard library से।
Pre-release 0.1.0 - package registry पर प्रकाशन जारी है
- Package
stampmitra- संस्करण
- 0.1.0
- आवश्यकता
- Ruby 3.0 या उससे नया (CI CRuby 3.0 से 4.0 पर चलता है)। सिर्फ़ standard library: कोई runtime dependency नहीं (net/http, json, openssl, securerandom)। Gem का नाम stampmitra है और module StampMitra है।
- लाइसेंस
- MIT
- Changelog
- CHANGELOG.md
- कोई runtime dependency नहीं: सिर्फ़ standard library।
- Immutable typed responses; जो fields gem नहीं जानता वे भी रहते हैं और [] व to_h से मिलते हैं।
- Jitter वाले backoff के साथ retries, और webhook replay को छोड़कर हर POST पर अपने आप Idempotency-Key।
- Lazy Enumerator pagination, typed webhook events, और Logger के ज़रिए redacted debug logging।
Package इंस्टॉल करें
gem install stampmitragem 'stampmitra', '~> 0.1.0'प्रकाशित होने के बाद उपलब्ध। अभी RubyGems पर प्रकाशित नहीं हुआ है। यह कमांड package के रिलीज़ होने के बाद काम करेगा; तब तक रिलीज़ के लिए GitHub repository देखते रहें।
gem 'stampmitra', git: 'https://github.com/Bani-Global-Industries-LLP/stampmitra-developer-ruby', branch: 'main'तब तक GitHub से इंस्टॉल करें: gemspec repository के root में है, इसलिए Bundler gem को सीधे Git से build कर सकता है। यह तभी काम करेगा जब repository की main branch प्रकाशित हो जाए।
पाँच पंक्तियों में PAN verify करें
बनाते समय sandbox key (sk_test_...) इस्तेमाल करें। यह मुफ़्त है, आपके वॉलेट से कुछ नहीं कटता और sandbox डेटा पर चलती है।
require 'stampmitra'
client = StampMitra::Client.new(api_key: ENV['STAMPMITRA_API_KEY']) # or just StampMitra::Client.new
pan = client.verify.pan(pan: 'ABCDE1234F', name: 'Rahul Kumar')
puts "#{pan.valid? ? 'valid' : 'invalid'} #{pan.name_match_result} #{pan.reference_id}"API keys और environments
Sandbox
Sandbox keys sk_test_ से शुरू होती हैं। ये मुफ़्त हैं और sandbox डेटा लौटाती हैं।
Live
Live keys sk_live_ से शुरू होती हैं। इनके लिए स्वीकृत production access request चाहिए और हर सफल कॉल पर शुल्क लगता है।
Key STAMPMITRA_API_KEY environment variable से पढ़ी जाती है, जब तक आप api_key: न दें। जो key ^sk_(test|live)_[A-Za-z0-9_-]{8,}$ से मेल नहीं खाती वह अस्वीकार होती है, जब तक आप allow_custom_key: true न दें। client.environment का मान 'test', 'live' या 'custom' होता है। base_url https होना चाहिए (plain http सिर्फ़ localhost, 127.0.0.1 और ::1 के लिए) और उसमें credentials नहीं होने चाहिए। API key inspect, to_s, pp, YAML, error messages या logs में कभी नहीं आती।
client = StampMitra::Client.new(
api_key: key,
base_url: 'https://api-b2b.stampmitra.in',
timeout: 30, # seconds, per attempt
max_retries: 2,
default_headers: { 'X-Trace-Id' => 'abc' },
app_info: { name: 'my-app', version: '1.0.0', url: 'https://example.com' },
logger: Logger.new($stderr),
validate_input: true,
allow_custom_key: false
)
# Every resource method also takes a trailing options: hash for that call only:
client.orders.get('ord_1', options: { timeout: 5, max_retries: 0, headers: { 'X-Trace-Id' => 'abc' } })
# options: { timeout:, max_retries:, idempotency_key:, headers: }हर resource एक जगह
Responses immutable value objects हैं (StampMitra::Models::PanVerification आदि) जिनमें snake_case readers हैं (pan.registered_name, pan.valid?)। अज्ञात fields कभी नहीं छूटते: pan[:referenceId], pan.unknown_fields या pan.to_h इस्तेमाल करें। inspect और to_s identifiers को mask करते हैं और नाम हटा देते हैं। जिन endpoints का आकार API ने तय नहीं किया (e-Stamp reference data, duty, parties, webhook events) वे Models::GenericResponse लौटाते हैं, और services.list Models::Service का Enumerable लौटाता है। ऑर्डर उस external_order_id से पहचाने जाते हैं जो create लौटाता है (कोई id नहीं होती)। हर typed नतीजे के साथ उसका response भी रहता है: order.response.status।
# Verification (every call, including valid: false results, is billed)
client.verify.pan(pan: 'ABCDE1234F', name: 'Rahul Kumar')
client.verify.gstin(gstin: '27ABCDE1234F1Z5')
client.verify.rc(vehicle_number: 'MH12AB1234')
client.verify.voter_id(voter_id: 'ABC1234567')
client.verify.driving_license(dl_number: 'MH1420110012345', dob: '1990-01-31') # IP-allowlisted key
client.verify.passport(file_number: 'PA1079341954215', dob: '1990-01-31') # IP-allowlisted key
client.verify.cin(cin: 'U74999DL2015PTC284009')
client.verify.udyam(udyam_number: 'UDYAM-MH-12-1234567')
client.verify.pan_to_gstin(pan: 'ABCDE1234F')
client.verify.pan_to_udyam(pan: 'ABCDE1234F')
client.verify.bank_account(account_number: '000123456789', ifsc: 'ICIC0001234', name: 'Rahul Kumar')
# Services and e-Stamp reference data
client.services.list.each { |service| puts service.service_code }
client.services.get('estamp.instrument.rent_agreement')
client.estamp.get_schema('estamp.instrument.rent_agreement')
client.estamp.list_states
client.estamp.list_instruments('MH')
client.estamp.list_articles('rent_agreement')
client.estamp.calculate_duty(state_code: 'MH', article_code: '5', stamp_duty_amount: 1000)
client.estamp.add_parties('ord_123', customer_name: 'Asha', party_name: 'Ravi')
# Orders and payments: orders are addressed by YOUR external_order_id
order = client.orders.create(
external_order_id: 'ord_1', external_customer_id: 'cust_1', service_code: 'estamp.instrument.rent_agreement',
options: { idempotency_key: 'order-ord_1-attempt-1' }
)
client.orders.get(order.external_order_id)
client.orders.get_status(order.external_order_id)
client.orders.get_payment(order.external_order_id)
client.orders.list_documents(order.external_order_id)
client.orders.get_document_download(order.external_order_id, 'doc_1') # the link is short-lived
client.payments.intents.create(external_order_id: 'ord_1', amount_minor: 50_000, currency: 'INR', payment_connection_id: 'pc_1')
client.payments.intents.get('pi_1')
# Usage, logs, webhook deliveries
client.usage.summary
client.logs.list(service: 'pan.verify', status: 'success', limit: 50)
client.webhooks.events.list(limit: 20)
client.webhooks.events.replay('evt_1')
# Raw HTTP metadata
order, response = client.with_response.orders.get('ord_1')
puts "#{response.status} #{response.request_id} #{response.attempts}"Pagination
logs.each next_cursor को lazily आगे बढ़ाता है, इसलिए first(10) को पर्याप्त नतीजे मिलते ही वह और पेज लाना बंद कर देता है। एक बार में एक पेज के लिए logs.list या each_page इस्तेमाल करें। webhooks.events.list limit: और cursor: लेता है।
client.logs.each(service: 'pan.verify', limit: 100) do |entry|
puts "#{entry.created_at} #{entry.service} #{entry.id}"
end
# Without a block, each returns an Enumerator, so it composes with lazy, first, take, each_slice...
latest = client.logs.each(limit: 50).lazy.select { |entry| entry.service == 'pan.verify' }.first(10)
page = client.logs.list(limit: 50) # page.items, page.next_cursor
first_page = client.logs.each_page(limit: 50).firstError classes
हर error StampMitra::Error से inherit होती है और status, code, message, request_id, field और body देती है। RateLimitError में retry_after जुड़ता है (सेकंड में, header मौजूद हो तब) और TimeoutError एक ConnectionError है। बाकी statuses base StampMitra::Error उठाते हैं, और non-JSON error body फिर भी status से map होती है।
| HTTP status | Class | टिप्पणी |
|---|---|---|
| 400 / local validation | StampMitra::ValidationError | गलत इनपुट। Validation helpers इसे request भेजने से पहले local रूप से भी उठाते हैं। |
| 401 | StampMitra::AuthenticationError | API key न मिली, गलत है या रद्द हो चुकी है। |
| 402 | StampMitra::PaymentRequiredError | वॉलेट में बैलेंस या क्रेडिट कम है। |
| 403 | StampMitra::PermissionDeniedError | सर्वर का संदेश जस का तस दिया जाता है, जैसे Passport और Driving Licence के लिए IP-allowlisted key चाहिए। |
| 404 | StampMitra::NotFoundError | ऑर्डर, सेवा या इवेंट की id मौजूद नहीं है। |
| 409 | StampMitra::ConflictError | Idempotency-Key किसी दूसरी body के साथ दोबारा इस्तेमाल हुई, या state में टकराव है। |
| 429 | StampMitra::RateLimitError | रुककर दोबारा कोशिश करें; सर्वर Retry-After भेजे तो वह मान उपलब्ध रहता है। |
| 5xx | StampMitra::UpstreamError | सर्वर या upstream की ओर की विफलता। 502, 503 और 504 पर अपने आप retry होता है। |
| network / timeout | StampMitra::ConnectionError / StampMitra::TimeoutError | कोई उपयोगी response नहीं मिला: कनेक्शन विफल, timeout या cancel। |
| webhook verification | StampMitra::SignatureVerificationError | गलत या पुराना webhook signature, खाली secret, या गलत timestamp। डिलीवरी को 400 से अस्वीकार करें। |
begin
client.verify.gstin(gstin: '27ABCDE1234F1Z5')
rescue StampMitra::RateLimitError => e
sleep(e.retry_after || 1) # the API sends no Retry-After today, so this is usually nil
rescue StampMitra::PermissionDeniedError => e
warn "not allowed: #{e.message}"
rescue StampMitra::Error => e
warn "#{e.class} #{e.status} #{e.code} request_id=#{e.request_id}: #{e.message}"
endRetries और idempotency
- डिफ़ॉल्ट max_retries 2 है (अधिकतम 3 attempts), exponential backoff और full jitter के साथ (0.5 s base, 8 s cap)। Retry-After header (सेकंड या HTTP तारीख़, अधिकतम 60 s) भेजा जाए तो उसका पालन होता है। timeout (डिफ़ॉल्ट 30 s) हर attempt पर लागू होता है, कुल समय-सीमा के रूप में नहीं।
- Retry होते हैं: 429, 502, 503, 504, connection errors और timeouts। Retry नहीं होते: बाकी 4xx और 5xx statuses, और redirects (जो कभी follow नहीं होते)। GET हमेशा retry के लिए सुरक्षित है।
- Gem हर उस POST के लिए (verification, duty calculation, e-Stamp parties, orders और payment intents) जिस पर API key स्वीकार करता है, random UUIDv4 Idempotency-Key बनाता है (CSPRNG से) और हर attempt पर वही key दोहराता है। इसलिए retry हुआ verification, order या payment intent दोबारा शुल्क या दोबारा निर्माण के बजाय पहला नतीजा लौटाता है। अपनी key options: { idempotency_key: '...' } से दें।
- Webhook replay (webhooks.events.replay) में अपने आप key नहीं लगती और key दिए बिना वह retry नहीं होता। Retries बंद करने के लिए client पर या एक कॉल पर max_retries: 0 दें। हर request नया कनेक्शन इस्तेमाल करती है और Net::HTTP का अपना transparent retry बंद है, इसलिए कोई request चुपचाप दोबारा नहीं भेजी जाती।
क्लाइंट-साइड validation
भेजने से पहले इनपुट जाँचे जाते हैं (PAN, GSTIN, IFSC, CIN, Udyam, vehicle number, 9 से 18 अंकों का account number, passport file number, driving licence, voter ID, phone, ISO तारीखें, नाम)। मान trim और upper-case होते हैं, पर separators हटाए नहीं जाते: "MH 12 AB 1234" अस्वीकार होता है, ठीक जैसे API करता है। विफलता पर StampMitra::ValidationError उठता है जिसमें field भरा होता है, बिना network call और बिना शुल्क के। Validators public हैं (StampMitra::Validators.pan(' abcde1234f ') "ABCDE1234F" लौटाता है); सब कुछ validate_input: false से बंद करें।
Webhook signatures जाँचें
हर डिलीवरी में X-Webhook-Signature, X-Webhook-Timestamp (Unix सेकंड) और X-Webhook-Event-Id होते हैं। Signature, timestamp, एक बिंदु और raw body का hex HMAC-SHA256 है, जो आपके endpoint secret से बनता है। जाँच हमेशा raw body पर करें, दोबारा serialise किए गए JSON पर नहीं।
Verification StampMitra::Webhook में है: construct_event_from_headers (Rack env, किसी भी header casing वाला Hash, या Rails का request.headers लेता है), construct_event(payload:, timestamp:, signature:, secret:) और boolean verify_signature। तुलना constant-time में होती है, अभी से 300 s (tolerance:) से ज़्यादा दूर के timestamps अस्वीकार होते हैं, खाली secret अस्वीकार होता है, और विफलता पर SignatureVerificationError उठता है (e.code देखें), जिसमें secret कभी नहीं होता।
payload = request.body.read # Rack/Sinatra; in Rails use request.raw_post
begin
event = StampMitra::Webhook.construct_event_from_headers(
payload: payload, headers: request.env, secret: ENV.fetch('STAMPMITRA_WEBHOOK_SECRET')
)
puts "#{event.type} #{event.core_order_id} delivery=#{event.event_id}"
rescue StampMitra::SignatureVerificationError => e
warn "rejected: #{e.code}" # respond 400
endआठों order.* events typed classes में decode होते हैं (Webhook::OrderCreated, OrderPaymentConfirmed, OrderFulfilmentStarted, OrderFulfilmentCompleted, OrderFailed, OrderCancelled, OrderRefundCompleted, OrderDocumentReady), हर एक में core_order_id, core_event_type, occurred_at और event_id हैं; अतिरिक्त fields event[:field] और event.to_h से मिलते हैं। अपरिचित coreEventType एक सादा Webhook::Event देता है, error नहीं। Raw body को ठीक वैसे ही दें जैसे मिली, दोबारा serialise किया JSON कभी नहीं। डिलीवरी retry होती है, इसलिए X-Webhook-Event-Id की event id से duplicates हटाएँ।
Framework के उदाहरण
# app/controllers/stampmitra_webhooks_controller.rb (Rails 6.1+)
# config/routes.rb: post '/webhooks/stampmitra', to: 'stampmitra_webhooks#create'
class StampmitraWebhooksController < ApplicationController
skip_forgery_protection # server-to-server call; the signature is the only authentication
def create
event = StampMitra::Webhook.construct_event_from_headers(
payload: request.raw_post, # the bytes as received, never params
headers: request.headers,
secret: Rails.application.credentials.dig(:stampmitra, :webhook_secret)
)
case event
when StampMitra::Webhook::OrderDocumentReady
FetchDocumentsJob.perform_later(event.core_order_id, event.event_id)
when StampMitra::Webhook::OrderFailed, StampMitra::Webhook::OrderCancelled
Rails.logger.warn("StampMitra #{event.type} for #{event.core_order_id}")
end
head :ok
rescue StampMitra::SignatureVerificationError => e
render json: { error: e.code }, status: :bad_request
end
endWebhooks: इवेंट के प्रकार, payload और retry नीति Webhooks पेज पर हैं।
सुरक्षित logging
डिफ़ॉल्ट रूप से कुछ भी log नहीं होता। Requests और responses के debug logs के लिए logger: में standard Logger दें (debug का जवाब देने वाला कोई भी object चलता है)। Authorization और x-api-key redact रहते हैं, और bodies में identifiers (PAN, GSTIN, account numbers) व नाम masked रहते हैं। Ruby 3.5 और नए में logger एक bundled gem है: Logger देते समय अपने Gemfile में gem 'logger' जोड़ें।
इंटीग्रेट करने के दूसरे तरीके
REST API किसी भी भाषा से चलती है, और बाकी SDKs भी उसी contract पर बने हैं।
API कॉल करने के लिए तैयार हैं?
मुफ़्त sandbox key लें और ऊपर का quickstart लगभग 5 मिनट में चलाएँ।