Integrating a GST Verification API with Salesforce
Validate customer and vendor GSTINs inside Salesforce using Apex callouts, named credentials and a verification API.
A GST verification API Salesforce integration is built from four standard pieces: a Named Credential backed by an External Credential to store the API key securely, an Apex callout class that calls the endpoint, a Queueable job so bulk verification does not hit governor limits, and a trigger on Account that enqueues verification when a GSTIN field changes. Below is the full pattern, including the test class Salesforce requires before you can deploy it.
The architecture: Named Credential first
Before writing any Apex, set up the connection layer. Salesforce's recommended pattern for outbound callouts is a Named Credential pointing at an External Credential, rather than storing endpoint URLs and secrets inside Apex or Custom Settings. This keeps the API key out of source control, lets an admin rotate it without a deployment, and lets Apex code reference the endpoint by name instead of a literal URL that would otherwise need updating across every class that calls it.
Step 1: External Credential and Named Credential
Create an External Credential with authentication protocol set to "Custom" and a parameter for the API key header. Then create a Named Credential that references this External Credential, with the URL set to the base endpoint.
| Setting | Value |
|---|---|
| External Credential — Authentication Protocol | Custom |
| External Credential — Custom Parameter | X-API-Key (type: header, sensitive) |
| Named Credential — URL | https://gstinapi.com |
| Named Credential — Generate Authorization Header | Disabled (the header is sent as a custom parameter, not OAuth) |
Once saved, Apex references the endpoint as callout:Your_Named_Credential/api/get-taxpayer-info/
followed by the GSTIN, and Salesforce automatically attaches the stored API key header at request time.
Step 2: an Apex callout class
A minimal synchronous callout class, wrapped so it can be reused from both a Queueable job and an anonymous test:
public with sharing class GstinVerificationService {
public class GstinVerificationException extends Exception {}
public static Map<String, Object> verify(String gstin) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Gstin_Api/api/get-taxpayer-info/' + gstin);
req.setMethod('GET');
req.setTimeout(20000);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() != 200) {
throw new GstinVerificationException(
'GST API returned ' + res.getStatusCode() + ': ' + res.getBody()
);
}
return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
}
}
The X-API-Key header does not appear in this class at all — it is injected by the Named
Credential named Gstin_Api in this example. That separation is the entire point: this class
can be unit tested, reused, and read by anyone without ever exposing the key.
Step 3: Queueable vs @future for bulk verification
For verifying a handful of Account records, an @future(callout=true) method is simple
enough. But @future methods only accept primitive parameter types (no sObjects, no
collections of objects) and cannot be chained, which becomes a problem the moment you need to verify more
Account IDs than fit in one transaction's callout limit. Queueable Apex solves both problems: it accepts
an arbitrary parameter object implementing Queueable, and one Queueable job can enqueue the
next inside its own execute method, letting you page through a large batch of Accounts
without exceeding the 100-callouts-per-transaction limit.
public class BulkGstinVerificationQueueable implements Queueable, Database.AllowsCallouts {
private List<Id> accountIds;
public BulkGstinVerificationQueueable(List<Id> accountIds) {
this.accountIds = accountIds;
}
public void execute(QueueableContext context) {
List<Id> batch = new List<Id>();
List<Id> remainder = new List<Id>();
for (Integer i = 0; i < accountIds.size(); i++) {
if (i < 50) {
batch.add(accountIds[i]);
} else {
remainder.add(accountIds[i]);
}
}
List<Account> accounts = [
SELECT Id, Gstin__c FROM Account WHERE Id IN :batch
];
for (Account acc : accounts) {
try {
Map<String, Object> result = GstinVerificationService.verify(acc.Gstin__c);
// map result fields onto acc here, then update
} catch (Exception e) {
// log and continue; do not let one failed callout stop the batch
}
}
update accounts;
if (!remainder.isEmpty() && !Test.isRunningTest()) {
System.enqueueJob(new BulkGstinVerificationQueueable(remainder));
}
}
}
Step 4: a trigger on Account
Apex triggers run synchronously as part of the DML transaction and cannot make callouts directly.
Instead, an after insert, after update trigger on Account should collect the IDs of records
whose GSTIN field changed and enqueue a Queueable job to verify them asynchronously, after the triggering
transaction has committed:
trigger AccountGstinTrigger on Account (after insert, after update) {
List<Id> toVerify = new List<Id>();
for (Account acc : Trigger.new) {
Account oldAcc = Trigger.isUpdate ? Trigger.oldMap.get(acc.Id) : null;
Boolean gstinChanged = oldAcc == null || acc.Gstin__c != oldAcc.Gstin__c;
if (gstinChanged && String.isNotBlank(acc.Gstin__c)) {
toVerify.add(acc.Id);
}
}
if (!toVerify.isEmpty()) {
System.enqueueJob(new BulkGstinVerificationQueueable(toVerify));
}
}
This is the same "verify at the point of entry" pattern covered in our broader guide on GST verification for vendor onboarding — the CRM record is a natural point to catch a bad or cancelled GSTIN before it reaches invoicing or payment.
Step 5: testing with HttpCalloutMock
Salesforce does not allow real HTTP callouts inside test methods, so every class that performs a callout
needs a corresponding mock. Implement HttpCalloutMock to return a canned JSON body, then
register it with Test.setMock before invoking the code under test:
@isTest
public class GstinVerificationServiceTest {
private class MockActiveResponse implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('{"taxpayer_data": {"name": "Reliance Industries Limited", "status": "Active"}}');
return res;
}
}
@isTest
static void testVerifyReturnsActiveStatus() {
Test.setMock(HttpCalloutMock.class, new MockActiveResponse());
Test.startTest();
Map<String, Object> result = GstinVerificationService.verify('27AAACR5055K1Z7');
Test.stopTest();
Map<String, Object> taxpayer = (Map<String, Object>) result.get('taxpayer_data');
System.assertEquals('Active', taxpayer.get('status'));
}
}
Write a second mock class returning a non-200 response to cover the exception path in
GstinVerificationService.verify, and a third simulating a timeout if your callout logic has
retry behaviour worth exercising.
Callout governor limits to design around
| Limit | Why it matters here |
|---|---|
| Callouts per transaction | Commonly cited as 100 — this is why bulk verification must chain Queueable jobs across batches rather than looping in one transaction |
| Callout timeout | Set explicitly with setTimeout; a slow verification response should fail fast rather than exhaust the transaction's execution time |
| Maximum callout size (request/response) | A GSTIN with an unusually long filing history — some large taxpayers return hundreds of filing records — can produce a larger response body worth accounting for if you store the raw payload |
For a language-agnostic look at the same rate-limit and retry concerns from the API side rather than the Salesforce side, see GST API rate limits, retries and webhooks.
Permission sets and who can trigger a callout
A Named Credential is not automatically usable by every user or every piece of Apex — access to it is controlled through permission sets in orgs using the newer Named Credential and External Credential model. Create a permission set that grants access to the specific External Credential and its principal, and assign it to the integration user or the profile that runs the Queueable jobs described above. This matters in practice because a class that compiles and works for a system administrator can fail silently for a standard user profile that was never granted access to the credential, which is a confusing failure mode to debug after the fact if you have not deliberately checked for it.
It is also worth deciding early who is allowed to add or change the GSTIN field on an Account in the first place — field-level security on the GSTIN field, combined with the trigger logic above, is what actually determines when a verification callout fires at all.
Logging and monitoring verification results
Because the Queueable job in this design swallows exceptions per-record so one bad callout does not stop
the whole batch, it is worth deliberately logging failures somewhere visible rather than letting a
catch block discard them silently. A simple approach is a custom object — something like a
Gstin_Verification_Log__c — with a row per attempt, storing the Account Id, the outcome, the
raw status code, and a timestamp. This gives you an audit trail for a Cancelled or Suspended finding, and
a queryable list of records that failed verification because of a transient API error rather than an
actually invalid GSTIN, which you would otherwise have to re-run manually. It also answers the question a
compliance reviewer will eventually ask: when was this vendor's GSTIN last checked, and what did the
check find.
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.