StampMitraDevelopers
आधिकारिक SDK

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
इंस्टॉल

Package इंस्टॉल करें

gem install
gem install stampmitra
Gemfile
gem 'stampmitra', '~> 0.1.0'

प्रकाशित होने के बाद उपलब्ध। अभी RubyGems पर प्रकाशित नहीं हुआ है। यह कमांड package के रिलीज़ होने के बाद काम करेगा; तब तक रिलीज़ के लिए GitHub repository देखते रहें।

Gemfile (from GitHub)
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 प्रकाशित हो जाए।

Quickstart

पाँच पंक्तियों में PAN verify करें

बनाते समय sandbox key (sk_test_...) इस्तेमाल करें। यह मुफ़्त है, आपके वॉलेट से कुछ नहीं कटता और sandbox डेटा पर चलती है।

Ruby
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}"
Authentication

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 में कभी नहीं आती।

सिर्फ़ server-side। sk_ keys को कभी browser या mobile ऐप में न डालें और न ही किसी repository में commit करें। इन्हें environment variables या secrets manager में रखें। Authentication
Client options
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: }
Resources

हर 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।

All resources
# 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

Pagination

logs.each next_cursor को lazily आगे बढ़ाता है, इसलिए first(10) को पर्याप्त नतीजे मिलते ही वह और पेज लाना बंद कर देता है। एक बार में एक पेज के लिए logs.list या each_page इस्तेमाल करें। webhooks.events.list limit: और cursor: लेता है।

Ruby
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).first
Errors

Error 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 statusClassटिप्पणी
400 / local validationStampMitra::ValidationErrorगलत इनपुट। Validation helpers इसे request भेजने से पहले local रूप से भी उठाते हैं।
401StampMitra::AuthenticationErrorAPI key न मिली, गलत है या रद्द हो चुकी है।
402StampMitra::PaymentRequiredErrorवॉलेट में बैलेंस या क्रेडिट कम है।
403StampMitra::PermissionDeniedErrorसर्वर का संदेश जस का तस दिया जाता है, जैसे Passport और Driving Licence के लिए IP-allowlisted key चाहिए।
404StampMitra::NotFoundErrorऑर्डर, सेवा या इवेंट की id मौजूद नहीं है।
409StampMitra::ConflictErrorIdempotency-Key किसी दूसरी body के साथ दोबारा इस्तेमाल हुई, या state में टकराव है।
429StampMitra::RateLimitErrorरुककर दोबारा कोशिश करें; सर्वर Retry-After भेजे तो वह मान उपलब्ध रहता है।
5xxStampMitra::UpstreamErrorसर्वर या upstream की ओर की विफलता। 502, 503 और 504 पर अपने आप retry होता है।
network / timeoutStampMitra::ConnectionError / StampMitra::TimeoutErrorकोई उपयोगी response नहीं मिला: कनेक्शन विफल, timeout या cancel।
webhook verificationStampMitra::SignatureVerificationErrorगलत या पुराना webhook signature, खाली secret, या गलत timestamp। डिलीवरी को 400 से अस्वीकार करें।
Ruby
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}"
end
Retries और idempotency

Retries और idempotency

Validation

क्लाइंट-साइड 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 से बंद करें।

Webhooks

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 कभी नहीं होता।

Ruby
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 के उदाहरण

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
end

Webhooks: इवेंट के प्रकार, payload और retry नीति Webhooks पेज पर हैं।

सुरक्षित logging

सुरक्षित 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' जोड़ें।

SDKs

इंटीग्रेट करने के दूसरे तरीके

REST API किसी भी भाषा से चलती है, और बाकी SDKs भी उसी contract पर बने हैं।

API कॉल करने के लिए तैयार हैं?

मुफ़्त sandbox key लें और ऊपर का quickstart लगभग 5 मिनट में चलाएँ।