Integration guide 9 min read

How to Integrate a GST Verification API in PHP

Step-by-step PHP code to call a GST verification API with cURL and Guzzle, and handle every GSTIN validation response.

Written and reviewed by the GSTIN API team Last reviewed

You can integrate a GST verification API in PHP with nothing more than the built-in cURL extension, or with Guzzle if your project already depends on it — both call the same endpoint and return the same JSON. This guide shows the raw cURL version first, then a Guzzle client with retries, then a PSR-18 abstraction so your application code is not locked to either library.

Validate the GSTIN before you call the API

A GSTIN is 15 characters, and the last one is a check digit computed from the first 14 using a Luhn mod-36 style algorithm. Validating this locally in PHP — before any HTTP call — rejects obviously malformed input for free. It does not prove the GSTIN is real; it only proves the string is well-formed, which is exactly why the live lookup later in this post still matters.


<?php

function gstinChecksumIsValid(string $gstin): bool
{
    $charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $gstin = strtoupper($gstin);

    if (strlen($gstin) !== 15) {
        return false;
    }

    $body = substr($gstin, 0, 14);
    $checkChar = $gstin[14];
    $sum = 0;

    for ($i = 0; $i < 14; $i++) {
        $pos = strpos($charset, $body[$i]);
        if ($pos === false) {
            return false;
        }

        $factor = ($i % 2 === 0) ? 1 : 2;
        $product = $pos * $factor;
        $sum += intdiv($product, 36) + ($product % 36);
    }

    $expected = $charset[(36 - ($sum % 36)) % 36];

    return $expected === $checkChar;
}

A raw cURL request

For a script, a cron job, or any codebase that should not carry an extra dependency for one API call, raw cURL against GET https://gstinapi.com/api/get-taxpayer-info/{gstin} is enough. Send the API key as the X-API-Key header, exactly as documented.


<?php

function getTaxpayerInfoCurl(string $gstin, string $apiKey): array
{
    if (! gstinChecksumIsValid($gstin)) {
        throw new InvalidArgumentException("'{$gstin}' is not a structurally valid GSTIN");
    }

    $ch = curl_init("https://gstinapi.com/api/get-taxpayer-info/{$gstin}");

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["X-API-Key: {$apiKey}"],
        CURLOPT_TIMEOUT => 10,
        CURLOPT_CONNECTTIMEOUT => 5,
    ]);

    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException("cURL request failed: {$error}");
    }

    if ($status < 200 || $status >= 300) {
        throw new RuntimeException("GST verification API returned HTTP {$status}: {$body}");
    }

    return json_decode($body, true, flags: JSON_THROW_ON_ERROR);
}

A Guzzle client with retries

Guzzle is the better fit once this call is one of several outbound integrations in a real application. Its handler stack lets you add retry middleware declaratively, instead of writing your own retry loop around cURL, and it gives connection pooling for free when you reuse a single client instance.


<?php

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\RequestInterface;

final class GstinClient
{
    private Client $http;

    public function __construct(private string $apiKey)
    {
        $stack = HandlerStack::create();

        $stack->push(Middleware::retry(
            function (int $retries, RequestInterface $request, ?ResponseInterface $response) {
                if ($retries >= 3) {
                    return false;
                }

                return $response && in_array($response->getStatusCode(), [429, 500, 502, 503, 504], true);
            },
            fn (int $retries) => 500 * (2 ** $retries)
        ));

        $this->http = new Client([
            'base_uri' => 'https://gstinapi.com/api/',
            'headers' => ['X-API-Key' => $this->apiKey],
            'timeout' => 10,
            'handler' => $stack,
        ]);
    }

    public function getTaxpayerInfo(string $gstin): array
    {
        if (! gstinChecksumIsValid($gstin)) {
            throw new InvalidArgumentException("'{$gstin}' is not a structurally valid GSTIN");
        }

        $response = $this->http->get("get-taxpayer-info/{$gstin}");

        return json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR);
    }
}

Reliance Industries’ public GSTIN, 27AAACR5055K1Z7, is a good one to try this against manually — its real response returns 354 filing records and 244 additional places of business, so it is a solid test of how your code handles a large payload before you point it at smaller businesses.

Abstracting behind PSR-18

If you want your GSTIN lookup code to be independent of Guzzle specifically, code it against Psr\Http\Client\ClientInterface and PSR-17 request/response factories instead of Guzzle’s concrete Client. Guzzle implements PSR-18 out of the box, so this costs almost nothing today and leaves you free to swap in Symfony’s HttpClient or another PSR-18 implementation later without rewriting the verification logic itself.


<?php

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;

final class Psr18GstinClient
{
    public function __construct(
        private ClientInterface $client,
        private RequestFactoryInterface $requestFactory,
        private string $apiKey,
    ) {}

    public function getTaxpayerInfo(string $gstin): array
    {
        if (! gstinChecksumIsValid($gstin)) {
            throw new InvalidArgumentException("'{$gstin}' is not a structurally valid GSTIN");
        }

        $request = $this->requestFactory
            ->createRequest('GET', "https://gstinapi.com/api/get-taxpayer-info/{$gstin}")
            ->withHeader('X-API-Key', $this->apiKey);

        $response = $this->client->sendRequest($request);

        if ($response->getStatusCode() >= 400) {
            throw new RuntimeException(
                "GST verification API returned HTTP {$response->getStatusCode()}"
            );
        }

        return json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR);
    }
}

Optionally, a plain DTO instead of a raw array

All three clients above hand back a decoded associative array, which is fine for a small script but starts to hurt once several parts of an application read from it — a typo in an array key fails silently at runtime instead of at compile time. If the codebase is large enough to care, map the array onto a plain readonly class before it leaves the client, so the rest of the application works against named properties instead of string keys.


<?php

final class TaxpayerInfo
{
    public function __construct(
        public readonly string $gstin,
        public readonly string $legalName,
        public readonly ?string $tradeName,
        public readonly string $status,
        public readonly string $registrationDate,
        public readonly ?string $cancellationDate,
        public readonly string $constitution,
        public readonly string $taxpayerType,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            gstin: $data['gstin'],
            legalName: $data['legal_name'],
            tradeName: $data['trade_name'] ?? null,
            status: $data['status'],
            registrationDate: $data['registration_date'],
            cancellationDate: $data['cancellation_date'] ?? null,
            constitution: $data['constitution'],
            taxpayerType: $data['taxpayer_type'],
        );
    }
}

This is optional — a straightforward script has no real need for it — but it pays off quickly once the same response is consumed by more than one part of the codebase, since PHP’s readonly properties give you a compile-time-checked shape instead of an untyped array passed around by convention.

Plain PHP versus a Laravel app

Everything above assumes a plain PHP application — a script, a Slim or Symfony app, or anything without a framework-provided HTTP client. If your codebase is Laravel, do not port this code as-is: Laravel’s HTTP client facade already wraps Guzzle with a fluent, testable API, and Form Requests are the idiomatic place to validate the GSTIN parameter before your controller ever calls the API. Rather than duplicate that guidance here, see how to integrate the GST verification API in Laravel for the Laravel-specific version of this same integration.

Handling errors and response shape

All three clients above return a decoded array (or you can map it to a DTO if you prefer typed objects). The response includes legal name, trade name, current status, registration and cancellation dates, constitution, taxpayer type, centre and state jurisdiction, e-invoicing status, principal and additional places of business, and the full GSTR filing history with per-return ARN and timeliness — see the full schema on the features page. Handle a 401 as an invalid or missing API key, a 404 as a GSTIN not found in the underlying registry, and a 429 as a rate limit that your retry logic should back off on, not retry immediately.

ApproachBest for
Raw cURLOne-off scripts, cron jobs, zero-dependency environments
GuzzleApplications that already depend on Guzzle or want built-in retry middleware
PSR-18 abstractionLibraries or codebases that must not be locked to one HTTP client
Laravel HTTP clientLaravel applications — see the dedicated Laravel guide
Which PHP approach fits which situation.

For high-volume verification — checking an entire vendor master rather than one GSTIN at a time — see the bulk GSTIN verification guide, and for the meaning of individual error responses, the API error codes guide.

Caching and rate-limit-friendly usage

Store the API key in an environment variable — through getenv(), a .env file, or your framework’s configuration layer — never hardcoded in a committed file. Set explicit connect and request timeouts on every client above, since none of PHP’s HTTP layers time out by default in a way you should rely on.

Because registration status, filing history, and e-invoicing eligibility do not change from one minute to the next, it is usually safe to cache a taxpayer lookup for a few hours using APCu, Redis, or your framework’s cache store, keyed on the GSTIN. For a business that checks the same handful of vendor GSTINs repeatedly — on every invoice, say — this alone can cut real credit spend substantially without any loss of accuracy that matters in practice.

Frequently asked questions

Either works for a single call. Raw cURL is fine for a one-off script with no dependencies. Guzzle is the better choice for an application, since it gives you middleware, retries, and connection pooling without hand-rolling them on top of cURL.
Check that the string is 15 characters from the allowed charset, then recompute the 15th character using the Luhn mod-36 check-digit algorithm and compare it. This is pure string and arithmetic logic with no HTTP call, so it costs nothing and catches typos before they consume a verification credit.
The HTTP call itself is identical — Laravel just gives you nicer tools to make it. Instead of raw cURL or a bare Guzzle client, you would typically use Laravel’s HTTP client facade and a Form Request for GSTIN validation; see our dedicated Laravel integration guide for that version.
PSR-18 is the PHP-FIG standard interface for HTTP clients. Coding your GST verification client against ClientInterface rather than against Guzzle directly means you can swap the underlying HTTP client — Guzzle, Symfony HttpClient, or another PSR-18 implementation — without touching your verification logic.
No. The check digit only proves the GSTIN string is well-formed; it does not prove the number was ever issued. Only a live call to the verification API confirms the GSTIN is registered and returns its current status.

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