Add a Smart Indian Address Checkout to Any React App in 10 Minutes

Author: QuantaRoute Geocoding Team Published: 2026-02-18
Use Case: Web App


Indian address is complex and unstructured. Your package never reach on time, because:

  • User can make wrong address entry while checking out
  • They type the wrong pincode
  • The form has no way to verify "Does a courier actually deliver here?"
  • Your backend ends up with half-baked "Near Big Tree, Opp. Blue Building" as a delivery address

This post shows you how to replace that with a two-step, Map-based, visually reliable flow that auto-fills everything from a map pin — in any React, Next.js, Vite, or Nuxt app.


What you're building

Step 1 — Map Pin: The user sees a live map, drags a pin to their exact location. The DigiPin code (India's official 10-character geocode) is displayed in real-time — offline, no API call needed.

Step 2 — Auto-fill form: One API call fills State, District, Locality, Pincode automatically. The user only types Flat number, Floor, and Building name (which is also pre-filled from OpenStreetMap if available).

The final output is a CompleteAddress object with coordinates, DigiPin, and a formatted address string — ready to send to your backend or payment gateway.


Prerequisites

  • Node.js 22+ and npm 10+
  • A React 18+ project (Next.js, Vite, Nuxt, or CRA)
  • A free QuantaRoute API key → developers.quantaroute.com

Free tier: 25,000 API requests per month. No credit card required.


Step 1 — Get an API key

  1. Sign up at developers.quantaroute.com
  2. Create a project and copy your API key
  3. Add it to your project's environment variables — never hard-code it in source files
FrameworkAdd this toVariable name
Next.js.env.localNEXT_PUBLIC_QUANTAROUTE_KEY
Vite.envVITE_QR_API_KEY
Nuxt 3.envNUXT_PUBLIC_QR_API_KEY
CRA.envREACT_APP_QR_API_KEY
# .env.local (Next.js example)
NEXT_PUBLIC_QUANTAROUTE_KEY=dp_6hldsfnlxxxxxxxxxxxxxxxx

Step 2 — Install the package

The widget uses MapLibre GL JS for the map (free, no API key needed). You install both:

npm install @quantaroute/checkout maplibre-gl

@quantaroute/checkout is a lightweight React component library (~40 KB ES module, ~9 KB gzipped — MapLibre loaded separately as a peer dependency).


Step 3 — Import the CSS

The package ships two CSS files you need to import once — in your app's global entry file:

import 'maplibre-gl/dist/maplibre-gl.css';        // MapLibre map styles
import '@quantaroute/checkout/style.css';          // Widget styles

Where you add this depends on your framework (see the per-framework guides below).

Why two imports? MapLibre is a peer dependency — it bundles its own CSS separately. The second import brings in the widget's UI styles.


Step 4 — Drop in the widget

import { CheckoutWidget } from '@quantaroute/checkout';

export default function CheckoutPage() {
  return (
    <CheckoutWidget
      apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
      onComplete={(address) => {
        // address.digipin         → "39J-438-TJC7"
        // address.lat / lng       → 28.6139, 77.2090
        // address.state           → "Delhi"
        // address.district        → "New Delhi"
        // address.locality        → "Nirman Bhawan SO"
        // address.pincode         → "110011"
        // address.flatNumber      → "4B"
        // address.buildingName    → "Sunshine Apartments"
        // address.formattedAddress → "4B, 3rd Floor, Sunshine Apartments, ..."
        console.log(address);
      }}
    />
  );
}

That's it. The widget handles everything: GPS location, map rendering, DigiPin, API calls, and form validation.


The CompleteAddress object

When the user confirms their address, onComplete fires with this object:

interface CompleteAddress {
  // Location identity
  digipin: string;          // India Post DigiPin (~4m × 4m precision)
  lat: number;              // Confirmed pin latitude
  lng: number;              // Confirmed pin longitude

  // Auto-filled from QuantaRoute API
  state: string;            // "Maharashtra"
  district: string;         // "Mumbai City"
  division: string;         // "Mumbai GPO"
  locality: string;         // "Churchgate SO"
  pincode: string;          // "400020"
  delivery: string;         // "Delivery" | "Non Delivery"
  country: string;          // "India"

  // Entered / pre-filled by user
  flatNumber: string;       // "12A"
  floorNumber: string;      // "4th"
  buildingName: string;     // "Maker Chambers" (pre-filled from OpenStreetMap)
  streetName: string;       // "Nariman Point, Marine Lines" (pre-filled from OSM)

  // Convenience field
  formattedAddress: string; // "12A, 4th Floor, Maker Chambers, Nariman Point, ..."
}

You now have everything you need to:

  • Store a verified, geocoded address in your database
  • Route a delivery driver using the exact coordinates
  • Deep-link to Google Maps, Apple Maps, or HERE Maps
  • Display a human-readable address on the order confirmation screen

Per-framework integration guides

Next.js — App Router (recommended)

The map uses browser APIs, so the component must be loaded client-side only.

1. Add CSS to your root layout:

// app/layout.tsx
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';
import type { Metadata } from 'next';

export const metadata: Metadata = { title: 'My Store' };

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

2. Create the checkout page with dynamic import (no SSR):

// app/checkout/page.tsx
'use client';

import dynamic from 'next/dynamic';
import type { CompleteAddress } from '@quantaroute/checkout';

const CheckoutWidget = dynamic(
  () => import('@quantaroute/checkout').then((m) => m.CheckoutWidget),
  { ssr: false }  // MapLibre requires the browser — never SSR
);

export default function CheckoutPage() {
  function handleComplete(address: CompleteAddress) {
    // Send to your order API
    fetch('/api/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ address }),
    });
  }

  return (
    <main className="max-w-lg mx-auto p-4">
      <CheckoutWidget
        apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
        onComplete={handleComplete}
        onError={(err) => console.error('Checkout error:', err)}
        title="Add Delivery Address"
      />
    </main>
  );
}

Next.js — Pages Router

// pages/checkout.tsx
import dynamic from 'next/dynamic';
import type { CompleteAddress } from '@quantaroute/checkout';
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';

const CheckoutWidget = dynamic(
  () => import('@quantaroute/checkout').then((m) => m.CheckoutWidget),
  { ssr: false }
);

export default function CheckoutPage() {
  function handleComplete(address: CompleteAddress) {
    console.log('DigiPin:', address.digipin);
    console.log('Formatted:', address.formattedAddress);
  }

  return (
    <div style={{ maxWidth: 520, margin: '0 auto', padding: 16 }}>
      <CheckoutWidget
        apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
        onComplete={handleComplete}
      />
    </div>
  );
}

Vite + React

// src/main.tsx (entry file)
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
// src/App.tsx
import { useState } from 'react';
import { CheckoutWidget } from '@quantaroute/checkout';
import type { CompleteAddress } from '@quantaroute/checkout';

function App() {
  const [savedAddress, setSavedAddress] = useState<CompleteAddress | null>(null);

  if (savedAddress) {
    return (
      <div>
        <h2>Order placed!</h2>
        <p>{savedAddress.formattedAddress}</p>
        <p>DigiPin: <strong>{savedAddress.digipin}</strong></p>
      </div>
    );
  }

  return (
    <CheckoutWidget
      apiKey={import.meta.env.VITE_QR_API_KEY}
      onComplete={setSavedAddress}
      onError={(err) => alert(err.message)}
      theme="light"
      mapHeight="360px"
    />
  );
}

export default App;

Nuxt 3

1. Register CSS globally in nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  css: [
    'maplibre-gl/dist/maplibre-gl.css',
    '@quantaroute/checkout/style.css',
  ],
  runtimeConfig: {
    public: {
      qrApiKey: process.env.NUXT_PUBLIC_QR_API_KEY,
    },
  },
});

2. Create a client-only component (.client.vue suffix = no SSR):

<!-- components/AddressWidget.client.vue -->
<script setup lang="ts">
import { CheckoutWidget } from '@quantaroute/checkout';
import type { CompleteAddress } from '@quantaroute/checkout';

const config = useRuntimeConfig();

const emit = defineEmits<{
  complete: [address: CompleteAddress];
}>();

function handleComplete(address: CompleteAddress) {
  emit('complete', address);
}
</script>

<template>
  <CheckoutWidget
    :api-key="config.public.qrApiKey"
    map-height="360px"
    title="Add Delivery Address"
    @complete="handleComplete"
  />
</template>

3. Use it in any page:

<!-- pages/checkout.vue -->
<script setup lang="ts">
import type { CompleteAddress } from '@quantaroute/checkout';

const savedAddress = ref<CompleteAddress | null>(null);
</script>

<template>
  <div class="max-w-lg mx-auto p-4">
    <AddressWidget @complete="savedAddress = $event" />
    <pre v-if="savedAddress">{{ savedAddress }}</pre>
  </div>
</template>

All props

PropTypeDefaultDescription
apiKeystringrequiredYour QuantaRoute API key
onComplete(addr: CompleteAddress) => voidrequiredFires when user saves address
onError(err: Error) => voidOptional error handler
apiBaseUrlstringhttps://api.quantaroute.comOverride for self-hosted API
defaultLatnumberIndia centerPre-center the map
defaultLngnumberIndia centerPre-center the map
theme'light' | 'dark''light'Color scheme
mapHeightstring'380px'CSS height of the map step
titlestring'Add Delivery Address'Header text
classNamestringExtra CSS class on root
styleCSSPropertiesInline styles on root
indiaBoundaryUrlstringURL to India boundary GeoJSON for map overlay

Advanced: Use DigiPin without the UI

If you just need the offline DigiPin algorithm (no UI, no API call):

import { getDigiPin, isWithinIndia, getLatLngFromDigiPin, isValidDigiPin } from '@quantaroute/checkout';

// Check if coordinates are within India before calling getDigiPin
// getDigiPin() throws if coordinates fall outside India's bounds — always guard it
if (isWithinIndia(28.6139, 77.2090)) {
  const digipin = getDigiPin(28.6139, 77.2090);
  // → "39J-438-TJC7"
}

// Decode a DigiPin back to center-point coordinates (also offline, no API call)
const { latitude, longitude } = getLatLngFromDigiPin('39J-438-TJC7');
// → { latitude: "28.613922", longitude: "77.208984" }

// Validate a DigiPin string (format + character check)
isValidDigiPin('39J-438-TJC7');  // → true
isValidDigiPin('INVALID');        // → false

Note: getDigiPin() throws an error if the coordinates are outside India's DigiPin coverage area (roughly 2.5°N–38.5°N, 63.5°E–99.5°E). Always call isWithinIndia() first, or wrap in a try/catch.

These are useful in delivery tracking, driver apps, or anywhere you receive GPS coordinates and need a human-readable DigiPin.


Advanced: useDigiPin hook

If you're building a custom map UI and only need the DigiPin reactive computation, the useDigiPin hook is the safe React-friendly wrapper — it returns null instead of throwing when coordinates are outside India:

import { useDigiPin } from '@quantaroute/checkout';

function MyMapOverlay({ lat, lng }: { lat: number; lng: number }) {
  const digipin = useDigiPin(lat, lng); // null if outside India

  if (!digipin) return <p>Move the pin inside India</p>;

  return <p>DigiPin: <strong>{digipin}</strong></p>;
}

The calculation is pure math (~0.1 ms), so it's safe to call on every map drag event without debouncing.


Advanced: Direct API access

For "bring your own UI" scenarios — where you want to call the QuantaRoute API yourself without using the widget — the package exports two API utilities:

import { lookupLocation, reverseGeocode } from '@quantaroute/checkout';

// Convert lat/lng → full Indian administrative address + DigiPin
const result = await lookupLocation(28.6139, 77.2090, process.env.YOUR_API_KEY);
// result.data.administrative_info → { state, district, division, locality, pincode, delivery, country }
// result.data.digipin → "39J-438-TJC7"

// Convert a DigiPin → address components from OpenStreetMap (building name, road, etc.)
const geo = await reverseGeocode('39J-438-TJC7', process.env.YOUR_API_KEY);
// geo.data.addressComponents → { name, road, suburb, city, postcode, ... }
// geo.data.displayName → "Nirman Bhawan, Rajpath, New Delhi"

Both functions throw descriptive errors for network timeouts, invalid API keys (401/403), and rate limit hits (429).


Advanced: Custom theme with CSS variables

Override the design tokens to match your brand:

/* Add to your global CSS */
.qr-checkout {
  --qr-primary:      #6366f1;   /* your brand colour */
  --qr-primary-dark: #4f46e5;
  --qr-radius:       6px;
  --qr-font:         'Inter', sans-serif;
}

Advanced: Dark mode

Pass theme="dark" or drive it from your app's theme state:

import { useTheme } from 'next-themes';
import { CheckoutWidget } from '@quantaroute/checkout';

export default function CheckoutPage() {
  const { resolvedTheme } = useTheme();

  return (
    <CheckoutWidget
      apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
      theme={resolvedTheme === 'dark' ? 'dark' : 'light'}
      onComplete={(addr) => console.log(addr)}
    />
  );
}

Advanced: India boundary overlay (legal compliance)

Indian government policy requires a recognisable India boundary on any public-facing map. The widget supports this via an optional GeoJSON overlay — a thin grey outline, no fill, loaded in parallel with the map so there is no perceived delay.

1. Download the GeoJSON file (India boundary, ~300 KB) and put it in your app's public/ folder:

public/
  geojson/
    india.geojson

2. Pass the URL to the widget:

<CheckoutWidget
  apiKey={...}
  onComplete={...}
  indiaBoundaryUrl="/geojson/india.geojson"
/>

The file is fetched once and cached by the browser. In Next.js and Vite, files in public/ are served as static assets automatically.


Sending the address to your backend

Here's a complete example of wiring the widget output to an order creation API:

import { CheckoutWidget } from '@quantaroute/checkout';
import type { CompleteAddress } from '@quantaroute/checkout';

export default function CheckoutPage() {
  async function handleComplete(address: CompleteAddress) {
    const response = await fetch('/api/orders/create', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        // Exact coordinates for routing and last-mile delivery
        coordinates: {
          lat: address.lat,
          lng: address.lng,
          digipin: address.digipin,
        },
        // Human-readable address for display
        delivery_address: {
          flat: address.flatNumber,
          floor: address.floorNumber,
          building: address.buildingName,
          street: address.streetName,
          locality: address.locality,
          pincode: address.pincode,
          district: address.district,
          state: address.state,
          formatted: address.formattedAddress,
        },
      }),
    });

    if (response.ok) {
      router.push('/order-confirmation');
    }
  }

  return (
    <CheckoutWidget
      apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
      onComplete={handleComplete}
    />
  );
}

Routing a delivery partner to the address

Because the widget gives you exact GPS coordinates, you can deep-link to any navigation app:

function openGoogleMaps(lat: number, lng: number) {
  window.open(`https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`);
}

function openAppleMaps(lat: number, lng: number) {
  window.open(`https://maps.apple.com/?daddr=${lat},${lng}&dirflg=d`);
}

function openHereMaps(lat: number, lng: number) {
  window.open(`https://wego.here.com/directions/drive/mylocation/${lat},${lng}`);
}

function openWaze(lat: number, lng: number) {
  window.open(`https://waze.com/ul?ll=${lat},${lng}&navigate=yes`);
}

This is significantly more accurate than routing to a typed address string — the driver gets a pin at the exact confirmed location, not the nearest road guess.


Frequently asked questions

Does it work on mobile browsers? Yes. The widget is mobile-first — it goes full-screen on small viewports and uses the browser's Geolocation API to detect the user's current position with a single tap.

Does it need an internet connection? The map and the DigiPin display both require network access (map tiles from Carto). The DigiPin calculation (lat/lng → DigiPin code) is done offline in the browser using the official India Post algorithm, so it updates instantly as the user drags the pin.

What map tiles does it use? Carto Positron vector tiles via MapLibre GL JS. Free for commercial use with attribution. No Google Maps, no Mapbox, no API key needed for the map itself.

Can I use this without React? Yes — via the UMD build. See the README on npm for the <script> tag approach.

Is the DigiPin algorithm open source? Yes. The official India Post DigiPin algorithm (Apache 2.0, developed by India Post, IIT Hyderabad, and ISRO NRSC) is bundled inside the package. Source: github.com/INDIAPOST-gov/digipin.


Troubleshooting

TypeError: Cannot read properties of undefined (reading 'Map')

MapLibre is browser-only. Make sure you're loading the component client-side — use dynamic(() => import(...), { ssr: false }) in Next.js, or .client.vue suffix in Nuxt.

Map is blank / white

You're missing the MapLibre CSS import. Ensure import 'maplibre-gl/dist/maplibre-gl.css' is in your entry file before any component renders.

onComplete fires but address fields are empty

The API key is missing or invalid. Check your environment variable name matches your framework's convention (e.g. NEXT_PUBLIC_ prefix in Next.js) and that .env.local is not committed to git.

Geolocation not working in development

Browsers block the Geolocation API on non-HTTPS origins. Use localhost (which is treated as secure) or set up a local HTTPS proxy.


Next steps


Built with MapLibre GL JS · Carto Positron basemap · India Post DigiPin algorithm (Apache 2.0)

© QuantaRoute — quantaroute.com