GSTIN API Bulk API

Verify a list of GSTINs as an asynchronous job.

Operations 5

POST /api/bulk/jobs Submit a list of GSTINs #
GET /api/bulk/jobs List recent jobs #
GET /api/bulk/jobs/{job_id} Check a job #
DELETE /api/bulk/jobs/{job_id} Cancel a running job #
GET /api/bulk/jobs/{job_id}/results Collect the results #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/gstinapi:gstinapi-bulk-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

gstinapi-bulk-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: GSTIN Bulk API
  version: 1.1.0
  summary: Verify Indian GST registration numbers, one at a time or in bulk.
  description: 'Live GSTIN verification against the Indian GST register, sourced through a GST

    Suvidha Provider network rather than by scraping the public portal.'
  termsOfService: https://gstinapi.com/terms
  license:
    name: Commercial — see terms of service
    url: https://gstinapi.com/terms
  contact:
    name: GSTIN API support
    url: https://gstinapi.com/contact
    email: contact@gstinapi.com
  x-documentation: https://gstinapi.com/docs
servers:
- url: https://gstinapi.com
  description: Production
security:
- ApiKeyAuth: []
tags:
- name: Bulk
  description: Verify a list of GSTINs as an asynchronous job.
paths:
  /api/bulk/jobs:
    post:
      tags:
      - Bulk
      operationId: createBulkJob
      summary: Submit a list of GSTINs
      description: 'Accepts up to 10000 GSTINs and returns immediately with a job ID; the

        list is verified in the background at a rate this service controls, so no

        client-side batching or rate limiting is needed.


        Supply either `gstins` as an array, or `text` as a blob to have the

        GSTINs pulled out of it — one per line, comma separated, or pasted

        straight from a spreadsheet column.


        Malformed entries are settled at submission without ever reaching the

        government source. Duplicates are verified once and charged once, but

        still appear as separate rows in the results, in the order submitted.


        An account may hold 3 unfinished jobs at once. Throttled to

        20 submissions per minute.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkJobRequest'
            examples:
              array:
                summary: An array of GSTINs
                value:
                  name: Vendor master
                  gstins:
                  - 27AAACR5055K1Z7
                  - 29AAACI1195H1ZI
              pasted:
                summary: A pasted spreadsheet column
                value:
                  text: '27AAACR5055K1Z7

                    29AAACI1195H1ZI

                    '
      responses:
        '202':
          description: Accepted and queued. Poll the job to follow it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '422':
          description: 'Nothing usable in the submission: `no_gstins` when no candidates were found, `too_many_gstins` when the list is over the per-job ceiling.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Either the submission throttle, or `too_many_active_jobs` when the account already holds the maximum number of unfinished jobs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      tags:
      - Bulk
      operationId: listBulkJobs
      summary: List recent jobs
      description: Jobs belonging to the account, newest first. Jobs are scoped to the account rather than the key, so a rotated key still reaches the jobs it started.
      parameters:
      - name: per_page
        in: query
        schema:
          type: integer
          default: 25
          minimum: 1
          maximum: 100
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
      responses:
        '200':
          description: A page of jobs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  jobs:
                    type: array
                    items:
                      $ref: '#/components/schemas/BulkJob'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/bulk/jobs/{job_id}:
    get:
      tags:
      - Bulk
      operationId: getBulkJob
      summary: Check a job
      description: 'The endpoint to poll. Every few seconds is fine — it reads one row and

        never touches the government source.


        Poll until `status` is `completed`, `cancelled` or `failed`. A completed

        job carrying `stopped_reason: insufficient_credits` ran out of balance

        part way through: the rows it never reached are marked `skipped` in the

        results and were not charged.'
      parameters:
      - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: The job as it stands.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/JobNotFound'
    delete:
      tags:
      - Bulk
      operationId: cancelBulkJob
      summary: Cancel a running job
      description: Rows already verified keep their results and remain charged. Nothing still pending is looked up or billed — those rows become `skipped`.
      parameters:
      - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Cancelled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job:
                    $ref: '#/components/schemas/BulkJob'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/JobNotFound'
        '409':
          description: '`already_finished` — the job had already completed, failed or been cancelled.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/bulk/jobs/{job_id}/results:
    get:
      tags:
      - Bulk
      operationId: getBulkResults
      summary: Collect the results
      description: 'One entry per submitted row, in the order submitted, paginated.


        Readable while the job is still running — rows appear as they are

        verified — so a client that wants to start processing early can. Pass

        `format=csv` for a spreadsheet instead of JSON, or `outcome` to take only

        certain rows.'
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: format
        in: query
        description: Pass `csv` to stream the results as a CSV download instead of JSON.
        schema:
          type: string
          enum:
          - json
          - csv
      - name: outcome
        in: query
        description: Comma-separated outcomes to include, e.g. `ok` for verified rows only, or `not_found,invalid_format` to review the rest.
        schema:
          type: string
          example: ok
      - name: per_page
        in: query
        schema:
          type: integer
          default: 100
          minimum: 1
          maximum: 1000
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
      responses:
        '200':
          description: A page of results, or the whole job as CSV.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job:
                    $ref: '#/components/schemas/BulkJob'
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/BulkResultRow'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
            text/csv:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/JobNotFound'
components:
  schemas:
    Pagination:
      type: object
      properties:
        page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer
        last_page:
          type: integer
        has_more:
          type: boolean
    BulkJob:
      type: object
      description: A submitted list and how far through it we are.
      properties:
        job_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
          - queued
          - processing
          - completed
          - cancelled
          - failed
          description: Terminal states are `completed`, `cancelled` and `failed`. Stop polling on any of them.
        stopped_reason:
          type:
          - string
          - 'null'
          enum:
          - insufficient_credits
          - cancelled
          - null
          description: Why a job ended before reaching every row. Null on a job that ran to completion normally.
        name:
          type:
          - string
          - 'null'
        source:
          type: string
          enum:
          - api
          - dashboard
        progress:
          type: object
          properties:
            percent:
              type: integer
              minimum: 0
              maximum: 100
            total_rows:
              type: integer
              description: Entries submitted, duplicates included.
            total_lookups:
              type: integer
              description: Distinct valid GSTINs — what actually reaches the government source, and the most this job can cost.
            completed_lookups:
              type: integer
        results:
          type: object
          description: Outcome tallies, counted over distinct GSTINs rather than rows.
          properties:
            succeeded:
              type: integer
            not_found:
              type: integer
            invalid:
              type: integer
            failed:
              type: integer
            skipped:
              type: integer
        credits_used:
          type: integer
          description: Credits charged so far. Equals `results.succeeded`.
        created_at:
          type: string
          format: date-time
        started_at:
          type:
          - string
          - 'null'
          format: date-time
        finished_at:
          type:
          - string
          - 'null'
          format: date-time
    BulkJobRequest:
      type: object
      description: Supply exactly one of `gstins` or `text`.
      properties:
        gstins:
          type: array
          items:
            type: string
            maxLength: 64
          minItems: 1
          maxItems: 10000
          description: The GSTINs to verify, in the order you want them back.
        text:
          type: string
          description: A blob to extract GSTINs from — one per line, comma separated, or pasted from a spreadsheet.
        name:
          type: string
          maxLength: 255
          description: A label for the job, to recognise it in the list later.
    BulkJobEnvelope:
      type: object
      properties:
        success:
          type: boolean
        job:
          $ref: '#/components/schemas/BulkJob'
        links:
          type: object
          description: Where to poll this job and collect its results.
          properties:
            self:
              type: string
              format: uri
            results:
              type: string
              format: uri
            csv:
              type: string
              format: uri
    TaxpayerProfile:
      type: object
      description: A verified registration, normalised. This is the shape bulk results carry; the single endpoint returns the raw envelope instead.
      properties:
        success:
          type: boolean
        gstin:
          type: string
        legal_name:
          type:
          - string
          - 'null'
        trade_name:
          type:
          - string
          - 'null'
          description: Falls back to the legal name when the business reports no separate trade name.
        status:
          type: string
          description: Active, Cancelled, Suspended, Provisional, …
        taxpayer_type:
          type:
          - string
          - 'null'
        constitution:
          type:
          - string
          - 'null'
        registration_date:
          type:
          - string
          - 'null'
          format: date
        cancellation_date:
          type:
          - string
          - 'null'
          format: date
        last_updated_at:
          type:
          - string
          - 'null'
          format: date
        active_years:
          type:
          - integer
          - 'null'
        nature_of_business:
          type: array
          items:
            type: string
        jurisdiction:
          type: object
          properties:
            centre:
              type:
              - string
              - 'null'
            centre_code:
              type:
              - string
              - 'null'
            state:
              type:
              - string
              - 'null'
            state_code:
              type:
              - string
              - 'null'
        principal_address:
          type:
          - object
          - 'null'
          properties:
            formatted:
              type: string
            building:
              type:
              - string
              - 'null'
            street:
              type:
              - string
              - 'null'
            locality:
              type:
              - string
              - 'null'
            district:
              type:
              - string
              - 'null'
            state:
              type:
              - string
              - 'null'
            pincode:
              type:
              - string
              - 'null'
            nature:
              type: array
              items:
                type: string
        additional_address_count:
          type: integer
        e_invoice:
          type: object
          properties:
            enabled:
              type: boolean
            generating:
              type: boolean
            turnover_slab:
              type:
              - string
              - 'null'
            eligibility:
              type:
              - string
              - 'null'
        filing_counts:
          type: object
          description: Count of filings held per return type.
          additionalProperties:
            type: integer
        derived:
          type: object
          description: Read from the GSTIN itself, not reported by the register.
          properties:
            state_code:
              type:
              - string
              - 'null'
            state_name:
              type:
              - string
              - 'null'
            pan:
              type:
              - string
              - 'null'
            pan_holder_type:
              type:
              - string
              - 'null'
            entity_code:
              type:
              - string
              - 'null'
            registration_number_in_state:
              type:
              - integer
              - 'null'
        source:
          type: object
          properties:
            fetched_at:
              type:
              - string
              - 'null'
              format: date-time
            served_from_cache:
              type: boolean
              description: The register itself refreshes a few times a day, so recent lookups are served from cache. Cached rows are charged the same as fresh ones.
    BulkResultRow:
      type: object
      description: One submitted row and what became of it.
      properties:
        row:
          type: integer
          description: Position in the submitted list, 1-based.
        input:
          type: string
          description: What was submitted for this row, verbatim.
        gstin:
          type:
          - string
          - 'null'
          description: The normalised GSTIN, or null if the entry was not a usable one.
        outcome:
          $ref: '#/components/schemas/Outcome'
        message:
          type:
          - string
          - 'null'
          description: Why this row is not `ok`. Null when it is.
        data:
          oneOf:
          - $ref: '#/components/schemas/TaxpayerProfile'
          - type: 'null'
          description: The verified profile. Present only when `outcome` is `ok`.
    Outcome:
      type: string
      enum:
      - ok
      - not_found
      - invalid_format
      - upstream_error
      - skipped
      - pending
      description: 'What happened to a row, and whether it cost anything:


        - `ok` — a live registration was found. **One credit.**

        - `not_found` — well-formed, but no registration exists. Free.

        - `invalid_format` — not a valid GSTIN; rejected on its checksum without contacting the government source. Free.

        - `upstream_error` — the government source could not answer. Free; resubmit the row.

        - `skipped` — the job was cancelled or ran out of credits before reaching this row. Free.

        - `pending` — not verified yet. Only seen while a job is still running.'
    Error:
      type: object
      properties:
        success:
          type: boolean
          const: false
        reason:
          type: string
          description: A stable code to branch on. The message is for people and may be reworded.
          enum:
          - no_gstins
          - too_many_gstins
          - too_many_active_jobs
          - insufficient_credits
          - already_finished
        message:
          type: string
      required:
      - success
      - message
  responses:
    PaymentRequired:
      description: The account has no credits left. Nothing was verified and nothing was charged.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            message: Insufficient credits
    Unauthorized:
      description: The API key was missing, unknown, or not active.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            message: Invalid or missing API key
    JobNotFound:
      description: No job with that ID on this account.
  parameters:
    JobId:
      name: job_id
      in: path
      required: true
      description: The `job_id` returned when the job was submitted.
      schema:
        type: string
        format: uuid
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Per-account key, created and rotated from the dashboard at https://gstinapi.com/settings/api-keys. New accounts receive 20 free credits on email verification.