Overview
The IPFly JavaScript SDK wraps the IPFly API in a single dependency-free file. It works as a browser global, a CommonJS require, or an ES module import, and it handles the operational details — caching, retries, rate limiting, and request de-duplication — so your integration code can stay focused on what to do with the response.
TTL + LRU caching
Response cache backed by memory, localStorage, or sessionStorage — configurable TTL and max size.
Automatic retries
Exponential backoff with jitter on network errors and 5xx responses. 4xx fails fast, no wasted retries.
Request de-duplication
Two calls for the same IP in the same tick share a single network request.
Batch lookups
Look up many IPs with bounded concurrency; a bad IP never fails the whole batch.
Client-side rate limiting
A token-bucket limiter smooths bursts so you don't blow through your plan's requests/second.
Zero dependencies
One UMD file — browser global, CommonJS, or ES module. MIT licensed.
Installation
No npm package is published — host ipfly-sdk.js yourself, alongside your other static assets or on your own CDN, and reference it by URL.
<script src="https://ipfly.world/libs/javascript-sdk/1.0.0.js"></script>
<script>
const ipfly = IPFly.createClient({ token: 'YOUR_TOKEN' });
</script>const IPFly = require('./1.0.0.js');
const ipfly = IPFly.createClient({ token: 'YOUR_TOKEN' });import IPFly from './1.0.0.js';
const ipfly = IPFly.createClient({ token: 'YOUR_TOKEN' });Quick Start
Create a client once, then call lookup() wherever you need geolocation data. Every method returns a Promise.
const ipfly = IPFly.createClient({
token: 'YOUR_TOKEN',
include: 'security' // optional: request the security/ASN field set on every call
});
// Look up a specific IP
ipfly.lookup('8.8.8.8').then((data) => {
console.log(data.city, data.country_name, data.security.is_vpn);
});
// Look up the caller's own IP (omit the ip argument)
ipfly.lookupSelf().then((data) => {
console.log('You appear to be in', data.city);
});
// async/await works the same way
async function whoIsThis(ip) {
try {
return await ipfly.lookup(ip);
} catch (err) {
console.error(err.code, err.message);
}
}Security Note
Any token embedded in client-side JavaScript is visible to anyone who opens dev tools or views page source. This SDK runs in the browser because that's what makes it useful for drop-on-your-site use cases, but for a production public website you have two safe options.
1. Proxy it (recommended)
Run a thin backend endpoint that holds the real token, and point the SDK's baseURL at your own endpoint instead of https://ipfly.world/api. Your backend forwards the request to IPFly and returns the result — the token never reaches the browser.
2. Restrict the token's origin
In your IPFly dashboard, lock the token to your site's specific domain(s). A copied or stolen token then simply won't work from anywhere else. Treat this as a mitigation, not a substitute for proxying.
Prototyping? Calling IPFly directly from the browser with a domain-restricted token is a reasonable tradeoff. Shipping to production with meaningful traffic? Proxy it. See Examples below for a proxy pattern.
Client Options
All options are passed to IPFly.createClient({ ... }).
| Option | Type | Default | Description |
|---|---|---|---|
token | string | required | Your IPFly API token. |
baseURL | string | ipfly.world/api | Override to point at your own backend proxy. |
include | string | null | Default include value sent with every request (e.g. "security"). Overridable per call. |
timeout | number | 8000 | Per-request timeout in ms before the request is aborted. |
retries | number | 2 | Retry attempts for network errors and 5xx. 4xx is never retried. |
retryDelay | number | 300 | Base delay (ms) for exponential backoff; jitter is added automatically. |
rateLimit | number | 0 | Max requests/second this client will issue. 0 = unlimited. |
concurrency | number | 6 | Default max parallel requests for batchLookup. |
cache.enabled | boolean | true | Set false to disable caching entirely. |
cache.ttl | number | 300000 | How long a cached response stays valid, in ms (5 minutes). |
cache.maxSize | number | 500 | Max cached entries before least-recently-used entries are evicted. |
cache.storage | string | 'memory' | 'memory', 'localStorage', or 'sessionStorage'. Web Storage persists across page loads. |
fetch | function | global fetch | Supply your own implementation (e.g. node-fetch) for non-browser environments. |
debug | boolean | false | Log internal lifecycle events to the console (tokens are redacted). |
onRequest | function | null | (ip, url) => void — fired right before a network request is sent. |
onResponse | function | null | (ip, data, fromCache) => void — fired after a successful lookup. |
onError | function | null | (ip, error) => void — fired when a lookup ultimately fails, after retries. |
onCacheHit | function | null | (ip, data) => void — fired when a cached response is served instead of a network call. |
.lookup() & .lookupSelf()
client.lookup(ip?, options?) looks up a single IP. Omit ip (or pass null/undefined) to geolocate the caller — or use the client.lookupSelf(options?) shorthand.
client.lookup('1.1.1.1', {
include: 'security', // overrides the client-level `include` for this call only
skipCache: true, // force a fresh network request, bypassing the cache
signal: abortController.signal // optional AbortSignal to cancel this specific call
});Returns a Promise that resolves with the parsed JSON response (see Response Shape) or rejects with an IPFlyError (see Error Handling).
.batchLookup()
client.batchLookup(ips, options?) looks up an array of IPs with bounded concurrency (options.concurrency overrides the client default). This call never rejects — each entry in the resolved array is a success or failure record, so one bad IP doesn't stop the batch.
const results = await client.batchLookup(['8.8.8.8', '1.1.1.1', 'not-an-ip']);
// [
// { ip: '8.8.8.8', ok: true, data: { ... } },
// { ip: '1.1.1.1', ok: true, data: { ... } },
// { ip: 'not-an-ip', ok: false, error: IPFlyError }
// ]Cache Controls
| Method | Returns | Description |
|---|---|---|
client.clearCache() | void | Empties the response cache immediately. |
client.cacheStats() | object | Returns { enabled, size, ttl, maxSize } — useful for a debug or admin/status panel. |
client.destroy() | void | Clears the cache and any in-flight request bookkeeping. Call this when tearing down a client instance (e.g. on a route change in a single-page app). |
IPFly.isValidIP()
A standalone helper that validates an IPv4 or IPv6 string without making a request.
IPFly.isValidIP('8.8.8.8'); // true
IPFly.isValidIP('2001:4860:4860::8888'); // true
IPFly.isValidIP('not-an-ip'); // falseResponse Shape
Successful lookups resolve with the same JSON structure the IPFly API returns, unchanged — see the full field reference in the API docs. Shape depends on your plan and the include value requested.
{
"ip": "8.8.8.8",
"hostname": "dns.google",
"country_name": "United States",
"city": "Mountain View",
"latitude": "37.4056",
"longitude": "-122.0775",
"time_zone": { "name": "America/New_York", "...": "..." },
"asn": { "asn": "AS15169", "name": "Google LLC", "...": "..." },
"security": { "is_vpn": false, "is_tor": false, "...": "..." }
}Error Handling
All failures reject with an IPFlyError (a standard Error subclass) carrying message, status, code, and cause fields.
| Code | Meaning |
|---|---|
| MISSING_TOKEN | No token was supplied when creating the client. |
| NO_FETCH | No fetch implementation is available; supply one via config.fetch. |
| INVALID_IP | The IP string failed local validation before any request was sent. |
| TIMEOUT | The request exceeded timeout or was aborted. |
| NETWORK_ERROR | The request failed before a response was received (offline, DNS, CORS, etc). |
| HTTP_ERROR | The API responded with a non-2xx status — check status (401 = bad token, 403 = plan/permission issue, 404 = private/bogon IP, 429/5xx = retry-worthy). |
| BAD_RESPONSE | The server responded but the body wasn't valid JSON. |
try {
await client.lookup('8.8.8.8');
} catch (err) {
if (err.code === 'TIMEOUT') {
// retry later, show a "slow connection" message, etc.
} else if (err.status === 401) {
// token is invalid — surface a config error, don't retry
}
console.error(err.message);
}Performance Notes
Caching
Avoids re-querying the same IP within the TTL window — handy for pages that look up the same visitor repeatedly, or dashboards re-rendering a list of known IPs.
De-duplication
If three components fire lookup('1.2.3.4') in the same tick, only one network request goes out; all three get the same result.
Rate limiting
A courtesy limiter on the client side that smooths bursts (e.g. rendering a table of 200 rows) so you don't blow through your plan's requests/second in a single frame.
Retries
Only apply to transient failures (network errors, timeouts, 5xx). A 401/403/404 fails fast since retrying won't fix a bad token or a private IP.
Examples
Two common integration patterns: rendering a small piece of personalized UI, and proxying instead of exposing your token.
<script src="https://ipfly.world/libs/javascript-sdk/1.0.0.js"></script>
<script>
const ipfly = IPFly.createClient({
token: 'YOUR_TOKEN',
cache: { storage: 'sessionStorage', ttl: 30 * 60 * 1000 }
});
ipfly.lookupSelf()
.then((data) => {
document.getElementById('banner').textContent =
`Welcome from ${'$'}{data.city || data.country_name}!`;
})
.catch(() => {
document.getElementById('banner').textContent = 'Welcome!';
});
</script>// Point at your own backend, no token here at all
const ipfly = IPFly.createClient({
token: 'unused-placeholder', // your backend holds the real token
baseURL: '/api/geo' // your server-side route
});// Your backend, holding the real token server-side
app.get('/api/geo', async (req, res) => {
const ip = req.query.ip || req.ip;
const upstream = await fetch(
`https://ipfly.world/api?token=${'$'}{process.env.IPFLY_TOKEN}&ip=${'$'}{ip}&include=security`
);
res.status(upstream.status).json(await upstream.json());
});Browser Support
Any environment with Promise, Map, and fetch (or a supplied fetch polyfill/implementation) works — all evergreen browsers, and Node 18+ for server-side use. AbortController is used opportunistically for timeouts/cancellation and is optional: its absence just disables timeout support, it won't throw.
License
MIT — use it, fork it, ship it.