Getting the public IP in Flutter — no API key, no rate limits, no hassle
If you've ever built a Flutter app that needs to know the user's public IP address, you've probably gone through the same journey I did: pick a free service, add the key, hit the rate limit, look for another one, repeat.
This article shows a cleaner approach using IPPubblico.org — a completely free API with no key required, HTTPS, CORS enabled, and no rate limits for reasonable usage. I'll cover four common use cases with real code, from the simplest one-liner to full geolocation with country detection.
Why IPPubblico?
Before diving into code, here's why it's worth trying:
- No API key — zero setup, works immediately
- HTTPS only — no mixed content issues
- CORS enabled — works from web too
- Plain text endpoint — no JSON parsing for simple cases
- Full JSON endpoint — city, region, country, ISP, ASN, timezone when you need it
- Free for reasonable usage — no hidden quotas
Setup
Add http to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
http: ^1.2.0
And add internet permission to AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Use case 1 — Just the IP (simplest possible)
If you only need the raw IPv4 address, use the plain text endpoint:
import 'package:http/http.dart' as http;
Future<String?> getPublicIP() async {
try {
final response = await http
.get(Uri.parse('https://ipv4.ippubblico.org/'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
return response.body.trim();
}
} catch (e) {
debugPrint('Failed to get IP: $e');
}
return null;
}
Usage:
final ip = await getPublicIP();
print(ip); // 203.0.113.42
That's it. No parsing, no model classes, no authentication. The endpoint returns a single line with the IP address.
Use case 2 — IPv4 and IPv6 together
If you need to detect both protocols, use the ?text=1 endpoint on the main domain:
Future<Map<String, String?>> getBothIPs() async {
try {
final response = await http
.get(Uri.parse('https://ippubblico.org/?text=1'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final lines = response.body.trim().split('\n');
String? ipv4;
String? ipv6;
for (final line in lines) {
if (line.startsWith('IPv4: ')) {
final value = line.substring(6).trim();
if (value != 'NONE') ipv4 = value;
} else if (line.startsWith('IPv6: ')) {
final value = line.substring(6).trim();
if (value != 'NONE') ipv6 = value;
}
}
return {'ipv4': ipv4, 'ipv6': ipv6};
}
} catch (e) {
debugPrint('Failed to get IPs: $e');
}
return {'ipv4': null, 'ipv6': null};
}
Usage:
final ips = await getBothIPs();
print('IPv4: ${ips['ipv4']}'); // IPv4: 203.0.113.42
print('IPv6: ${ips['ipv6']}'); // IPv6: 2001:db8::1 or null
Use case 3 — Full geolocation data
When you need country, city, ISP and timezone, use the JSON endpoint:
import 'dart:convert';
import 'package:http/http.dart' as http;
class IPInfo {
final String ip;
final String? country;
final String? countryCode;
final String? city;
final String? region;
final String? isp;
final String? timezone;
final double? lat;
final double? lon;
IPInfo({
required this.ip,
this.country,
this.countryCode,
this.city,
this.region,
this.isp,
this.timezone,
this.lat,
this.lon,
});
factory IPInfo.fromJson(Map<String, dynamic> json) {
final geo = json['geo'] as Map<String, dynamic>? ?? {};
return IPInfo(
ip: json['ip'] ?? '',
country: geo['country'],
countryCode: geo['country_code'],
city: geo['city'],
region: geo['region'],
isp: json['isp'],
timezone: json['timezone'],
lat: (geo['lat'] as num?)?.toDouble(),
lon: (geo['lon'] as num?)?.toDouble(),
);
}
}
Future<IPInfo?> getIPInfo() async {
try {
final response = await http
.get(Uri.parse('https://ippubblico.org/?api=1'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return IPInfo.fromJson(json);
}
} catch (e) {
debugPrint('Failed to get IP info: $e');
}
return null;
}
Usage:
final info = await getIPInfo();
if (info != null) {
print('IP: ${info.ip}');
print('Country: ${info.country} (${info.countryCode})');
print('City: ${info.city}, ${info.region}');
print('ISP: ${info.isp}');
print('Timezone: ${info.timezone}');
}
Sample response:
{
"status": "ok",
"ip": "203.0.113.42",
"isp": "Example ISP",
"timezone": "Europe/Rome",
"geo": {
"city": "Milan",
"region": "Lombardy",
"country": "Italy",
"country_code": "IT",
"lat": 45.4654,
"lon": 9.1859
}
}
Use case 4 — Country detection for locale/content
A common real-world need: detect the user's country on first launch to set the default language or show region-specific content.
class CountryDetector {
static String? _cachedCountryCode;
static Future<String?> getCountryCode() async {
if (_cachedCountryCode != null) return _cachedCountryCode;
try {
final response = await http
.get(Uri.parse('https://ippubblico.org/?api=1'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
final geo = json['geo'] as Map<String, dynamic>? ?? {};
_cachedCountryCode = geo['country_code'] as String?;
return _cachedCountryCode;
}
} catch (e) {
debugPrint('Country detection failed: $e');
}
return null;
}
static void clearCache() {
_cachedCountryCode = null;
}
}
Usage in main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final countryCode = await CountryDetector.getCountryCode();
final locale = _localeFromCountry(countryCode);
runApp(MyApp(initialLocale: locale));
}
Locale _localeFromCountry(String? code) {
switch (code) {
case 'IT': return const Locale('it');
case 'DE': return const Locale('de');
case 'FR': return const Locale('fr');
case 'CN': return const Locale('zh');
case 'JP': return const Locale('ja');
default: return const Locale('en');
}
}
Handling errors gracefully
A real app should always have a fallback. Here's a production-ready wrapper that tries multiple strategies:
Future<String?> getPublicIPWithFallback() async {
// try plain text endpoint first (fastest)
try {
final response = await http
.get(Uri.parse('https://ipv4.ippubblico.org/'))
.timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
return response.body.trim();
}
} catch (_) {}
// fallback to JSON endpoint
try {
final response = await http
.get(Uri.parse('https://ippubblico.org/?api=1'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as Map<String, dynamic>;
return json['ip'] as String?;
}
} catch (_) {}
return null;
}
Respecting rate limits
IPPubblico is free and has no hard quotas, but like any public API it has soft rate limiting to prevent abuse. A few guidelines:
- Don't poll continuously — check the IP once at app startup and cache it
- Cache the result — an IP address rarely changes; re-check at most every few minutes if you really need it live
-
Handle 429 — if you get a
Too Many Requestsresponse, read theRetry-Afterheader and wait before retrying:
Future<String?> getIPRespectingRateLimit() async {
final response = await http
.get(Uri.parse('https://ipv4.ippubblico.org/'))
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
return response.body.trim();
}
if (response.statusCode == 429) {
final retryAfter = int.tryParse(
response.headers['retry-after'] ?? '20'
) ?? 20;
debugPrint('Rate limited. Retry after $retryAfter seconds.');
await Future.delayed(Duration(seconds: retryAfter));
// retry once
final retry = await http
.get(Uri.parse('https://ipv4.ippubblico.org/'))
.timeout(const Duration(seconds: 5));
if (retry.statusCode == 200) return retry.body.trim();
}
return null;
}
Quick reference
| Need | Endpoint | Response |
|---|---|---|
| IPv4 only | https://ipv4.ippubblico.org/ |
203.0.113.42 |
| IPv6 only | https://ipv6.ippubblico.org/ |
2001:db8::1 or NONE
|
| Both protocols | https://ippubblico.org/?text=1 |
IPv4: x.x.x.x\nIPv6: x |
| Full geolocation | https://ippubblico.org/?api=1 |
JSON with city, country, ISP |
Full API documentation: ippubblico.org/docs.html
Conclusion
IPPubblico covers everything from the simplest "just give me the IP" to full geolocation with a single API, no registration, no key management, no billing surprises. For Flutter apps that just need to know where their users are connecting from, it's the path of least resistance.
The Retry-After header handling is worth implementing even if you never hit the limit — it's good practice and makes your app resilient by design.
Have you used a different approach for IP detection in Flutter? Share it in the comments.
Top comments (0)