Integrating a GST Verification API in Ruby on Rails
Add real-time GSTIN verification to a Rails app using Faraday, a service object, and a validated ActiveRecord model.
The idiomatic way to call a GST verification API from a Rails app is a service object under app/services
that wraps a Faraday connection with retry middleware, plus, where you want it enforced at the model level, a
custom ActiveModel::Validator that checks a vendor's GSTIN on save. For anything beyond a handful
of lookups, push the work into a Sidekiq job rather than doing it inline in a controller action.
A service object for the API call
Rails does not ship an opinion on where third-party API calls live, but the community convention — and the
one that keeps this testable — is a plain old Ruby object under app/services, callable with a
simple class method. It takes the API key from Rails credentials or an environment variable, never from a
hardcoded string.
# app/services/gst_verification_service.rb
class GstVerificationService
class NotFoundError < StandardError; end
class UnauthorizedError < StandardError; end
BASE_URL = "https://gstinapi.com/api"
def self.call(gstin)
new.verify(gstin)
end
def verify(gstin)
response = connection.get("/get-taxpayer-info/#{gstin}")
case response.status
when 200
response.body
when 404
raise NotFoundError, "GSTIN #{gstin} not found"
when 401
raise UnauthorizedError, "Invalid API key"
else
raise "Unexpected response: #{response.status}"
end
end
private
def connection
Faraday.new(url: BASE_URL) do |f|
f.headers["X-API-Key"] = Rails.application.credentials.gstinapi_key
f.response :json
f.adapter Faraday.default_adapter
end
end
end
Calling it from anywhere else in the app is a single line: GstVerificationService.call("27AAACR5055K1Z7").
Because the class is small and side-effect-free apart from the HTTP call, it is easy to stub in a controller
test with allow(GstVerificationService).to receive(:call).
Faraday with retry middleware
A single dropped connection should not surface as an error to the end user. Add the
faraday-retry gem and configure it to retry only on the conditions where a retry can actually
help — timeouts and 5xx responses — with a short exponential backoff. Never retry a 404 or 401: a missing
GSTIN or a bad key will not change on a second attempt, and retrying it just wastes a request.
# Gemfile
gem "faraday"
gem "faraday-retry"
# app/services/gst_verification_service.rb
def connection
Faraday.new(url: BASE_URL) do |f|
f.headers["X-API-Key"] = Rails.application.credentials.gstinapi_key
f.request :retry, {
max: 2,
interval: 0.5,
backoff_factor: 2,
retry_statuses: [429, 500, 502, 503, 504],
exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed],
}
f.response :json
f.adapter Faraday.default_adapter
end
end
An ActiveRecord validator for vendor GSTINs
If your app has a Vendor model, a common pattern is to verify the GSTIN against the live API the
moment a vendor record is saved — catching a fat-fingered or cancelled GSTIN before it ever reaches your
accounts payable data. Wire it up as a custom
ActiveModel::Validator rather than inline validation logic, so it is reusable and easy to disable in
tests.
# app/validators/gstin_validator.rb
class GstinValidator < ActiveModel::Validator
def validate(record)
gstin = record.gstin
return if gstin.blank?
info = GstVerificationService.call(gstin)
unless info["status"] == "Active"
record.errors.add(:gstin, "is not an active GST registration (status: #{info['status']})")
end
rescue GstVerificationService::NotFoundError
record.errors.add(:gstin, "was not found in GST records")
rescue StandardError => e
Rails.logger.error("GSTIN verification failed: #{e.message}")
# Fail open rather than blocking a save on a transient API outage —
# decide deliberately whether your app should fail open or closed here.
end
end
# app/models/vendor.rb
class Vendor < ApplicationRecord
validates_with GstinValidator, on: :create
end
Running the check only on: :create (rather than on every update) avoids re-billing a lookup every
time an unrelated field on the vendor record changes — pair this with the caching approach described in
verifying a GST number via API if vendors are edited
often.
A Sidekiq job for bulk checks
Verifying an entire vendor master — dozens or hundreds of records — should never happen inline inside a web request; it will time out long before the last GSTIN is checked. Push it into a Sidekiq job instead, and enqueue one job per GSTIN (or a chunked batch) so Sidekiq's own concurrency settings, not a hand-rolled loop, control the pace.
# app/jobs/verify_vendor_gstin_job.rb
class VerifyVendorGstinJob
include Sidekiq::Job
sidekiq_options queue: "gst_verification", retry: 3
def perform(vendor_id)
vendor = Vendor.find(vendor_id)
info = GstVerificationService.call(vendor.gstin)
vendor.update!(
gst_status: info["status"],
gst_legal_name: info["legal_name"],
gst_last_checked_at: Time.current,
)
rescue GstVerificationService::NotFoundError
vendor.update!(gst_status: "not_found", gst_last_checked_at: Time.current)
end
end
# Enqueue a bulk run, e.g. from a rake task or admin action:
Vendor.pending_gst_check.find_each do |vendor|
VerifyVendorGstinJob.perform_async(vendor.id)
end
Give the queue its own name (gst_verification above) so you can size its concurrency
independently in config/sidekiq.yml — a low concurrency here keeps you under the API's
rate limit without throttling unrelated background jobs
elsewhere in the app. See the bulk verification guide for
patterns on chunking, storing results, and handling partial failures across a large run.
One more reason to keep the service and the validator separate: a fifteen-character GSTIN that passes
format validation (a Luhn mod-36 checksum over its first fourteen characters) is not proof the
business is real — it only proves the string is well-formed. Regex-only GSTIN validation is a common shortcut in
Rails apps, but it will happily accept a checksum-valid, entirely fictitious GSTIN. The live API call in
GstinValidator above is what actually confirms registration and status. See
GSTIN format explained for the full anatomy of the number.
Frequently asked questions
A note on accuracy. GST rules change often. This article reflects our understanding as of 30 August 2026 and is general information, not tax or legal advice. For the authoritative position, check gst.gov.in and cbic-gst.gov.in, or speak to a qualified tax professional about your specific situation.
Check a GSTIN right now, free
Five lookups a day, no account required. Status, filing history, e-invoicing and jurisdiction.
Free GST search toolBuild it into your product
One REST call, JSON back. Get an API key and 20 free verification credits in under a minute.
Start freeMore on developer integration guides
Browse every guide in the GST API blog.