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
- Sign up at developers.quantaroute.com
- Create a project and copy your API key
- Add it to your project's environment variables — never hard-code it in source files
| Framework | Add this to | Variable name |
|---|---|---|
| Next.js | .env.local | NEXT_PUBLIC_QUANTAROUTE_KEY |
| Vite | .env | VITE_QR_API_KEY |
| Nuxt 3 | .env | NUXT_PUBLIC_QR_API_KEY |
| CRA | .env | REACT_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
| Prop | Type | Default | Description |
|---|---|---|---|
apiKey | string | required | Your QuantaRoute API key |
onComplete | (addr: CompleteAddress) => void | required | Fires when user saves address |
onError | (err: Error) => void | — | Optional error handler |
apiBaseUrl | string | https://api.quantaroute.com | Override for self-hosted API |
defaultLat | number | India center | Pre-center the map |
defaultLng | number | India center | Pre-center the map |
theme | 'light' | 'dark' | 'light' | Color scheme |
mapHeight | string | '380px' | CSS height of the map step |
title | string | 'Add Delivery Address' | Header text |
className | string | — | Extra CSS class on root |
style | CSSProperties | — | Inline styles on root |
indiaBoundaryUrl | string | — | URL 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 callisWithinIndia()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
- Live demo → — see the widget in action
- npm package → — changelog, full type docs
- Get your API key → — free tier, no credit card
- Report an issue →
Built with MapLibre GL JS · Carto Positron basemap · India Post DigiPin algorithm (Apache 2.0)
© QuantaRoute — quantaroute.com