/** * gstinapi.in for Google Sheets — =GSTIN() custom function. * * =GSTIN(A2) legal name of the GSTIN in A2 * =GSTIN(A2, "status") Active / Cancelled / Suspended ... * =GSTIN(A2:A100, "status") a whole column in one formula * =GSTIN(A2, "all") one row: legal name, trade name, status, type, registered on, state, pincode * =GSTIN_VALID(A2) TRUE / FALSE, offline, free * * Cost: one credit per distinct, well-formed GSTIN that GSTN knows. A GSTIN with a * bad format or a bad check character is rejected here, before any call, so it * is free. Not-found and failed calls are free on the server. Nothing is cached: * Sheets only recalculates a cell when its inputs change. * * Install: Extensions > Apps Script > paste this file and appsscript.json > * reload the sheet > "GSTIN API" menu > Set API key. See README.md. */ var API_BASE = 'https://www.gstinapi.in'; var KEY_PROPERTY = 'GSTINAPI_KEY'; var CHUNK = 20; // parallel requests per fetchAll; the API allows 60 a minute by default var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var GSTIN_RE = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/; var FIELDS = { legal_name: 'Legal name', trade_name: 'Trade name', status: 'Status', taxpayer_type: 'Taxpayer type', business_constitution: 'Constitution', registration_date: 'Registered on', cancellation_date: 'Cancelled on', state_code: 'State code', state_jurisdiction: 'State jurisdiction', address: 'Address', pincode: 'Pincode' }; var ALL_FIELDS = ['legal_name', 'trade_name', 'status', 'taxpayer_type', 'registration_date', 'state_code', 'pincode']; /** Adds the menu. */ function onOpen() { SpreadsheetApp.getUi() .createMenu('GSTIN API') .addItem('Set API key', 'setApiKey') .addItem('Check connection', 'checkConnection') .addItem('Remove API key', 'removeApiKey') .addSeparator() .addItem('Get a free key (75 free credits)', 'openSignup') .addToUi(); } function setApiKey() { var ui = SpreadsheetApp.getUi(); var r = ui.prompt('gstinapi.in API key', 'Paste your key (starts with gak_). Create one at https://www.gstinapi.in/api-keys', ui.ButtonSet.OK_CANCEL); if (r.getSelectedButton() !== ui.Button.OK) return; var key = String(r.getResponseText() || '').trim(); if (!key) return; PropertiesService.getUserProperties().setProperty(KEY_PROPERTY, key); ui.alert('Saved. The key is stored for your Google account only; other people who open this sheet need their own key. If a cell already shows NO_KEY, click it and press Enter to recalculate.'); } function removeApiKey() { PropertiesService.getUserProperties().deleteProperty(KEY_PROPERTY); SpreadsheetApp.getUi().alert('API key removed.'); } function openSignup() { var html = HtmlService.createHtmlOutput('Create a free account at gstinapi.in').setWidth(320).setHeight(60); SpreadsheetApp.getUi().showModalDialog(html, 'Get an API key'); } /** Uses the free test GSTIN, so it never spends a credit. */ function checkConnection() { var ui = SpreadsheetApp.getUi(); var key = getKey_(); if (!key) { ui.alert('No API key yet. Use GSTIN API > Set API key.'); return; } var res = UrlFetchApp.fetch(API_BASE + '/v1/gstin/00AAAAA0000A1ZT', { headers: { 'x-api-key': key }, muteHttpExceptions: true }); var code = res.getResponseCode(); ui.alert(code === 200 ? 'Connected. Your key works.' : 'Not working: ' + describe_(code, safeJson_(res.getContentText()))); } /** * Looks up GSTINs on gstinapi.in. * * @param {string|string[][]} input A GSTIN, or a range of GSTINs. * @param {string} [field] legal_name (default), trade_name, status, taxpayer_type, registration_date, cancellation_date, state_code, state_jurisdiction, address, pincode, business_constitution, or "all". * @param {string} [apiKey] Optional key; by default the one saved with GSTIN API > Set API key. * @return The requested detail. * @customfunction */ function GSTIN(input, field, apiKey) { var f = String(field === undefined || field === null || field === '' ? 'legal_name' : field).toLowerCase().replace(/[\s-]+/g, '_'); if (f !== 'all' && !FIELDS.hasOwnProperty(f)) { return 'ERROR: unknown field "' + field + '". Use one of: ' + Object.keys(FIELDS).join(', ') + ', all'; } var key = apiKey ? String(apiKey).trim() : getKey_(); if (!key) return 'NO_KEY: use the GSTIN API menu > Set API key'; var isRange = Array.isArray(input); var cells = isRange ? [].concat.apply([], input) : [input]; var gstins = cells.map(normalise_); var results = lookupMany_(gstins, key); var fields = f === 'all' ? ALL_FIELDS : [f]; var rows = gstins.map(function (g) { if (g === '') return fields.map(function () { return ''; }); var r = results[g]; return fields.map(function (name) { return r.ok ? valueOf_(r.data, name) : (name === fields[0] ? r.code : ''); }); }); if (!isRange) return f === 'all' ? [rows[0]] : rows[0][0]; return rows; } /** * TRUE when the text is a well-formed GSTIN with a correct check character. * Works offline and never uses a credit. * * @param {string|string[][]} input A GSTIN, or a range of GSTINs. * @return TRUE or FALSE. * @customfunction */ function GSTIN_VALID(input) { if (Array.isArray(input)) return input.map(function (row) { return row.map(function (c) { return c === '' ? '' : isValidGstin_(normalise_(c)); }); }); return isValidGstin_(normalise_(input)); } // ─── internals ────────────────────────────────────────────────────────────── function getKey_() { try { return PropertiesService.getUserProperties().getProperty(KEY_PROPERTY) || ''; } catch (e) { return ''; } } function normalise_(v) { return String(v === undefined || v === null ? '' : v).replace(/\s+/g, '').toUpperCase(); } function checkChar_(first14) { var sum = 0; for (var i = 0; i < 14; i++) { var p = CHARS.indexOf(first14.charAt(i)) * (i % 2 === 0 ? 1 : 2); sum += Math.floor(p / 36) + (p % 36); } return CHARS.charAt((36 - (sum % 36)) % 36); } function isValidGstin_(g) { return GSTIN_RE.test(g) && checkChar_(g.substring(0, 14)) === g.charAt(14); } /** @return {Object} gstin -> {ok, code, data}. Distinct GSTINs are fetched once per call. */ function lookupMany_(gstins, key) { var out = {}; var toFetch = []; gstins.forEach(function (g) { if (g === '' || out[g]) return; if (!GSTIN_RE.test(g)) { out[g] = { ok: false, code: 'INVALID_FORMAT' }; return; } if (!isValidGstin_(g) && g !== '00AAAAA0000A1ZT') { out[g] = { ok: false, code: 'BAD_CHECK_DIGIT' }; return; } out[g] = { ok: false, code: 'PENDING' }; toFetch.push(g); }); for (var i = 0; i < toFetch.length; i += CHUNK) { var chunk = toFetch.slice(i, i + CHUNK); var responses = UrlFetchApp.fetchAll(chunk.map(function (g) { return { url: API_BASE + '/v1/gstin/' + g, method: 'get', headers: { 'x-api-key': key }, muteHttpExceptions: true }; })); responses.forEach(function (res, j) { var code = res.getResponseCode(); var body = safeJson_(res.getContentText()); out[chunk[j]] = (code === 200 && body && body.data) ? { ok: true, data: body.data } : { ok: false, code: errorCode_(code) }; }); } return out; } function errorCode_(status) { if (status === 401 || status === 403) return 'BAD_KEY'; if (status === 402) return 'NO_CREDITS'; if (status === 404) return 'NOT_FOUND'; if (status === 429) return 'RATE_LIMIT_RETRY'; if (status === 400) return 'INVALID_FORMAT'; return 'UPSTREAM_ERROR_RETRY'; } function describe_(status, body) { var base = { BAD_KEY: 'the key was refused. Check it at https://www.gstinapi.in/api-keys', NO_CREDITS: 'no credits left. Buy a pack at https://www.gstinapi.in/buy-credits', RATE_LIMIT_RETRY: 'too many requests in a minute. Wait and try again.' }[errorCode_(status)]; return (base || 'HTTP ' + status) + (body && body.error && !base ? ' (' + body.error + ')' : ''); } function valueOf_(data, name) { var v = data[name]; if (v === undefined || v === null) return ''; return Array.isArray(v) ? v.join(', ') : v; } function safeJson_(text) { try { return JSON.parse(text); } catch (e) { return null; } }