Integration guide 9 min read

GST Verification in Odoo

How to verify a partner's GSTIN inside Odoo with a server action or a small module that calls a GST verification API.

Written and reviewed by the GSTIN API team Last reviewed

Odoo keeps a partner's GSTIN in the vat field on res.partner, and with VAT validation on it will tell you the number is shaped like a GSTIN. It will not tell you whether the registration exists, whether it is still Active, or whether the legal name matches the company you are about to pay. This guide adds that check in Python: a "Verify GSTIN" button, stored results, an automation rule and a nightly re-check, plus an external-script route for Odoo Online, where custom modules are not allowed.

What Odoo checks on its own

With the Indian localisation (l10n_in) installed, the partner's vat field is the GSTIN, and Odoo uses it on invoices and GST reports. If VAT number validation is enabled, Odoo also checks the number's structure when you save. That catches typos. It cannot catch a well-formed number that was cancelled last quarter.

Recent releases of the Indian localisation have also added GSTIN status lookups through Odoo's paid in-app services. What those return, and what they cost, varies by version and edition, so check the release notes for yours. The pattern below works on any version where you control the Python code, and stores the evidence in fields you own.

QuestionOdoo format checkLive verification
Is it 15 characters in a valid pattern?YesYes
Was this GSTIN actually issued?NoYes
Is it Active, Suspended or Cancelled today?NoYes
Does the legal name match the partner?NoYes
Is the partner filing returns?NoYes
What Odoo answers on its own, and what needs a live lookup.

Choosing an approach by hosting

Where your Odoo runs decides most of this. Odoo.sh and self-hosted installs let you install your own Python module. Odoo Online does not.

ApproachHostingEffortNotes
Server action with Python code onlyAnyLowCannot make the HTTP call: no imports, no requests
Automation rule calling a module methodOdoo.sh, on-premiseLow, once the module existsVerifies on create or when vat changes
Custom module: fields, button, cronOdoo.sh, on-premiseMedium, about a dayThe complete solution; stores evidence on the partner
External script over the external APIOdoo Online, Odoo.sh, on-premiseMediumNeeds somewhere to run it and plan-level API access
Approach, hosting and effort.

Whichever you pick, the lookup is one call: GET https://gstinapi.com/api/get-taxpayer-info/ followed by the GSTIN, with your key in an X-API-Key header. The response carries the registration under taxpayer_data and the recent GSTR filings under filing_data. The full reference is in the API docs, and the ERP integration guide covers where the check belongs in the vendor lifecycle whatever your ERP.

Approach 1: an automation rule or server action

The obvious first attempt is a server action of type "Execute Python Code" that does import requests and calls the API. It fails. Server action code is evaluated in Odoo's restricted sandbox, which rejects import statements, and requests is not one of the names the sandbox provides. Self-hosting does not lift that restriction; what self-hosting and Odoo.sh give you is the ability to install a module.

What sandboxed code can do is call a public method on the records it was given. So the split is: the HTTP call lives in a model method (Approach 2 below), and the automation rule is one line. Create a rule on Contacts, trigger it on creation and on update with vat as the watched field, and give it an "Execute Code" action:


records._verify_gstin()

The method writes only its own fields, so it does not re-fire a rule that watches vat. It does run inside the save, so the user waits for one round trip. If your team creates partners through bulk imports, leave this rule off and let the scheduled action catch up, so an import of 2,000 rows does not become 2,000 sequential HTTP calls in one transaction.

Approach 2: a small custom module

The module inherits res.partner, adds four fields and exposes a method that does the lookup. The API key is read from a system parameter, gstin_verify.api_key, which an administrator sets under Settings, Technical, System Parameters with developer mode on. Nothing secret goes in the code. Create the key from your account after you sign up.


# gstin_verify/__manifest__.py
{
    "name": "GSTIN Verification",
    "version": "17.0.1.0.0",   # match your series: 16.0 / 17.0 / 18.0
    "depends": ["base"],
    "data": ["views/res_partner_views.xml", "data/ir_cron.xml"],
    "license": "LGPL-3",
}

# gstin_verify/models/res_partner.py
import logging
import re

import requests

from odoo import fields, models
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)

API_URL = "https://gstinapi.com/api/get-taxpayer-info/%s"
GSTIN_SHAPE = re.compile(r"^[0-9]{2}[0-9A-Z]{13}$")


class ResPartner(models.Model):
    _inherit = "res.partner"

    gst_check = fields.Selection(
        [("verified", "Verified"), ("not_found", "Not found"), ("unchecked", "Could not check")],
        string="GSTIN check", readonly=True, copy=False,
    )
    gst_status = fields.Char("GST status", readonly=True, copy=False)
    gst_legal_name = fields.Char("GST legal name", readonly=True, copy=False)
    gst_verified_at = fields.Datetime("GSTIN verified at", readonly=True, copy=False)

    def _gstin_api_key(self):
        return self.env["ir.config_parameter"].sudo().get_param("gstin_verify.api_key")

    def _verify_gstin(self):
        """Verify each partner's GSTIN. Never raises on API trouble.
        Returns a problem code ('no_key', 'auth', 'credits') or None."""
        key = self._gstin_api_key()
        if not key:
            return "no_key"
        for partner in self:
            gstin = (partner.vat or "").replace(" ", "").upper()
            if not GSTIN_SHAPE.match(gstin):
                continue  # malformed: let Odoo's validation complain, spend nothing
            try:
                resp = requests.get(API_URL % gstin, headers={"X-API-Key": key}, timeout=(5, 15))
            except requests.RequestException as exc:
                _logger.warning("GSTIN %s: lookup failed: %s", gstin, exc)
                partner.write({"gst_check": "unchecked"})
                continue
            if resp.status_code == 401:
                return "auth"      # bad key: stop, every call will fail
            if resp.status_code == 402:
                return "credits"   # out of credits: stop the batch
            if resp.status_code != 200:  # 502 = government source unreachable, retry later
                _logger.warning("GSTIN %s: HTTP %s", gstin, resp.status_code)
                partner.write({"gst_check": "unchecked"})
                continue
            try:
                data = resp.json().get("taxpayer_data") or {}
            except ValueError:
                partner.write({"gst_check": "unchecked"})
                continue
            if data.get("status_code") == 1:
                partner.write({
                    "gst_check": "verified",
                    "gst_status": data.get("status"),        # e.g. Active, Cancelled
                    "gst_legal_name": data.get("name"),      # legal name
                    "gst_verified_at": fields.Datetime.now(),
                })
            else:  # status_code 0: no registration behind this GSTIN
                partner.write({
                    "gst_check": "not_found",
                    "gst_status": False,
                    "gst_legal_name": False,
                    "gst_verified_at": fields.Datetime.now(),
                })
        return None

    def action_verify_gstin(self):
        problem = self._verify_gstin()
        messages = {
            "no_key": "Set the system parameter gstin_verify.api_key first.",
            "auth": "The GSTIN API rejected the key (401). Check gstin_verify.api_key.",
            "credits": "The GSTIN API account is out of credits (402).",
        }
        if problem:
            raise UserError(messages[problem])
        return True

Four details carry most of the weight. The timeout=(5, 15) tuple caps the connect and read phases separately, so a hung connection cannot hold an Odoo worker. Network errors, 502s and unparseable bodies are recorded as "Could not check" and the loop moves on, so nothing ever blocks a save. A 401 or 402 stops the batch, because every following call would fail the same way. And a GSTIN with no registration behind it comes back as HTTP 200 with taxpayer_data.status_code set to 0, so the code reads that field before it trusts name or status. The full list of responses is in the GST API error codes guide.

Only the button raises, and only for problems a person can get fixed. Raising a UserError rolls back the transaction, which is fine for a button click and wrong for an automation rule, which is why the rule calls _verify_gstin() directly and ignores the return value.

The shape check is deliberately loose. For the full check-digit validation before you spend a call, the GSTIN validation library post has it in plain Python, and the Python package wraps validation and the API call if you would rather not maintain the client. Session reuse and retry patterns are in the Python integration guide.

The view puts the button and results next to the GSTIN on the partner form:




  
    res.partner.form.gstin.verify
    res.partner
    
    
      
        
        
        
        
        

Re-verifying on a schedule

A GSTIN that was Active at onboarding can be suspended or cancelled months later, and nothing in Odoo will tell you. A scheduled action (ir.cron) fixes that. Add this method to the same class; it checks never-verified partners first, then the oldest verifications, 200 a night:


    def _cron_reverify_gstin(self, batch=200):
        base = [("vat", "!=", False), ("parent_id", "=", False), ("country_id.code", "=", "IN")]
        todo = self.search(base + [("gst_verified_at", "=", False)], limit=batch)
        if len(todo) < batch:
            todo |= self.search(
                base + [("gst_verified_at", "!=", False)],
                order="gst_verified_at asc", limit=batch - len(todo),
            )
        problem = todo._verify_gstin()
        if problem:
            _logger.error("GSTIN re-verification stopped: %s", problem)



  
    GSTIN: re-verify partners
    
    code
    model._cron_reverify_gstin()
    1
    days
    True
  

Size the batch to your supplier count and credit budget. At ₹0.50 a lookup, 200 a night is about ₹3,000 a month, and only lookups that find a registration are charged; see pricing for the lower rate on larger purchases. Pacing and backoff are covered in rate limits and webhooks, and daily vendor GSTIN monitoring covers what to do when a re-check flips a supplier to Cancelled.

Odoo Online: verify from outside

On Odoo Online you cannot install the module, and a code-only server action cannot make the call. What remains is a script running elsewhere, on a small server or a scheduled cloud job, that reads partners through Odoo's external API, verifies them and writes the result back. External API access on Odoo Online depends on your subscription plan, so confirm yours includes it before you build.


import xmlrpc.client
import requests

URL, DB, USER, ODOO_KEY = "https://yourco.odoo.com", "yourco", "[email protected]", "..."
GSTIN_KEY = "..."

uid = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/common").authenticate(DB, USER, ODOO_KEY, {})
odoo = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/object")

partners = odoo.execute_kw(
    DB, uid, ODOO_KEY, "res.partner", "search_read",
    [[["vat", "!=", False], ["parent_id", "=", False], ["country_id.code", "=", "IN"]]],
    {"fields": ["id", "vat"], "limit": 200},
)

for p in partners:
    gstin = p["vat"].replace(" ", "").upper()
    try:
        r = requests.get(f"https://gstinapi.com/api/get-taxpayer-info/{gstin}",
                         headers={"X-API-Key": GSTIN_KEY}, timeout=(5, 15))
    except requests.RequestException:
        continue                      # network trouble: try again next run
    if r.status_code in (401, 402):
        raise SystemExit(f"Stopping: HTTP {r.status_code}")
    if r.status_code != 200:
        continue                      # 502 and friends: try again next run
    data = r.json().get("taxpayer_data") or {}
    if data.get("status_code") == 1:
        note = f"GSTIN verified: {data.get('status')}, legal name {data.get('name')}"
    else:
        note = "GSTIN not found in the GST register"
    odoo.execute_kw(DB, uid, ODOO_KEY, "res.partner", "message_post",
                    [[p["id"]]], {"body": note})

Posting to the partner's chatter gives you a dated audit trail without custom fields. If you have Odoo Studio, add fields for status, legal name and verified-at and write to those instead, so you can filter and group on them. Newer Odoo releases are moving away from XML-RPC towards a replacement API, so check the external API documentation for your version before you commit to it.

Odoo 16, 17 and 18 differences

The model code is plain ORM and should run unchanged across these versions. The differences are in views, UI names and a few fields, so check these against your version rather than assuming:

  • View modifiers. Odoo 17 replaced the attrs and states attributes with direct expressions such as invisible="not vat". Use attrs on 16 and the direct form on 17 and later. The view above uses neither, so it installs on all three.
  • List views. Odoo 18 renamed the <tree> view type to <list>. If you add the GSTIN fields to a list view, use the tag your version expects.
  • Automation naming. Odoo 16 calls them Automated Actions; Odoo 17 renamed them Automation Rules and reworked the editor. Both need the base_automation module, and the trigger-on-vat setup is the same idea in each.
  • Scheduled action fields. Older versions expose extra ir.cron fields, such as a call count, that newer ones dropped. If the cron XML fails to load, compare it with a core module's cron file in your version.
  • Manifest version. Prefix the module version with your series (16.0.x, 17.0.x, 18.0.x) so Odoo treats it as compatible.

Frequently asked questions

In the standard vat field on res.partner, which the Indian localisation (l10n_in) labels and uses as the GSTIN. Keep it on the company record rather than on child contacts, because Odoo reads tax identifiers from the commercial partner when it builds invoices.
Not from the server action code alone. Python code in a server action runs in a restricted sandbox that rejects import statements, and the requests library is not among the names it provides. The working pattern is to put the HTTP call in a model method inside a small module, then have the server action or automation rule call that method, which is only possible where you can install modules: Odoo.sh or a self-hosted install.
Odoo Online does not let you install custom Python modules, so the module approach is not available there. You can still verify partners from outside Odoo with a script that reads and writes res.partner through the external API, provided your subscription plan includes external API access.
Only if you run it inside the save, and then by one API round trip. The module in this guide uses a short timeout and catches every network error, so a slow or failed call marks the partner as not checked instead of blocking the save. If your team imports partners in bulk, leave verification to the button and the scheduled action.
Re-check active suppliers on a schedule rather than once at onboarding, because a registration can be suspended or cancelled long after the partner was created. A daily scheduled action that re-checks the oldest verifications in small batches keeps the whole list fresh without a burst of calls.
No. The VAT check confirms the number is well-formed, not that it was issued or is still active. Only a live lookup against the GST register tells you the registration status and the legal name behind it.

A note on accuracy. GST rules change often. This article reflects our understanding as of 23 September 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