Integration guide 8 min read

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.

Written and reviewed by the GSTIN API team Last reviewed

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

In a plain Ruby service object under app/services, not in a model callback or a controller action. Keeping it a small, single-purpose class makes it easy to stub in tests and to reuse from both a controller and a Sidekiq job.
Use Faraday with faraday-retry middleware configured for a small number of retries with exponential backoff, limited to safe conditions like timeouts and 5xx responses. Do not retry on a 404 (GSTIN not found) or 401 (bad API key) — those will not succeed on a second attempt.
Yes. A custom ActiveModel::Validator can call the verification API on save and add an error if the GSTIN is not found or not Active, so a bad vendor GSTIN never reaches your database in the first place.
In the background. Verifying hundreds of vendor GSTINs one request at a time inside a web request will time out; a Sidekiq job that processes them with a small concurrency limit keeps the request/response cycle fast and respects the API rate limit.

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 tool

Build it into your product

One REST call, JSON back. Get an API key and 20 free verification credits in under a minute.

Start free