apogeoapi 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -52
- package/EXAMPLES.md +666 -666
- package/README.md +532 -532
- package/dist/index.d.ts +11 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -14
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +4 -4
- package/dist/types/index.js.map +1 -1
- package/dist/utils/http-client.d.ts.map +1 -1
- package/dist/utils/http-client.js +5 -5
- package/dist/utils/http-client.js.map +1 -1
- package/package.json +44 -44
package/EXAMPLES.md
CHANGED
|
@@ -1,666 +1,666 @@
|
|
|
1
|
-
# SDK Usage Examples
|
|
2
|
-
|
|
3
|
-
Complete examples demonstrating how to use the Geo API SDK.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [Basic Setup](#basic-setup)
|
|
8
|
-
- [Authentication Flow](#authentication-flow)
|
|
9
|
-
- [Geography Queries](#geography-queries)
|
|
10
|
-
- [Account Management](#account-management)
|
|
11
|
-
- [API Keys Lifecycle](#api-keys-lifecycle)
|
|
12
|
-
- [Subscription Management](#subscription-management)
|
|
13
|
-
- [Webhook Setup](#webhook-setup)
|
|
14
|
-
- [Error Handling Patterns](#error-handling-patterns)
|
|
15
|
-
- [Production Best Practices](#production-best-practices)
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Basic Setup
|
|
20
|
-
|
|
21
|
-
### Node.js / Express Application
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import {
|
|
25
|
-
import express from 'express';
|
|
26
|
-
|
|
27
|
-
const app = express();
|
|
28
|
-
const client = new
|
|
29
|
-
apiKey: process.env.GEO_API_KEY!,
|
|
30
|
-
baseURL: process.env.GEO_API_URL || 'https://api.
|
|
31
|
-
timeout: 30000,
|
|
32
|
-
retries: 3
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
app.get('/countries', async (req, res) => {
|
|
36
|
-
try {
|
|
37
|
-
const countries = await client.geo.getCountries();
|
|
38
|
-
res.json(countries);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
res.status(500).json({ error: error.message });
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
app.listen(3000);
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### Next.js API Route
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
// pages/api/countries.ts
|
|
51
|
-
import {
|
|
52
|
-
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
53
|
-
|
|
54
|
-
const client = new
|
|
55
|
-
apiKey: process.env.GEO_API_KEY!
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export default async function handler(
|
|
59
|
-
req: NextApiRequest,
|
|
60
|
-
res: NextApiResponse
|
|
61
|
-
) {
|
|
62
|
-
try {
|
|
63
|
-
const countries = await client.geo.getCountries();
|
|
64
|
-
res.status(200).json(countries);
|
|
65
|
-
} catch (error: any) {
|
|
66
|
-
res.status(error.statusCode || 500).json({
|
|
67
|
-
error: error.message
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### React Hook
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// hooks/useGeoAPI.ts
|
|
77
|
-
import {
|
|
78
|
-
import { useState, useEffect } from 'react';
|
|
79
|
-
|
|
80
|
-
const client = new
|
|
81
|
-
apiKey: process.env.NEXT_PUBLIC_GEO_API_KEY!
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
export function useCountries() {
|
|
85
|
-
const [countries, setCountries] = useState([]);
|
|
86
|
-
const [loading, setLoading] = useState(true);
|
|
87
|
-
const [error, setError] = useState(null);
|
|
88
|
-
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
client.geo.getCountries()
|
|
91
|
-
.then(setCountries)
|
|
92
|
-
.catch(setError)
|
|
93
|
-
.finally(() => setLoading(false));
|
|
94
|
-
}, []);
|
|
95
|
-
|
|
96
|
-
return { countries, loading, error };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Usage in component
|
|
100
|
-
function CountriesList() {
|
|
101
|
-
const { countries, loading, error } = useCountries();
|
|
102
|
-
|
|
103
|
-
if (loading) return <div>Loading...</div>;
|
|
104
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<ul>
|
|
108
|
-
{countries.map(country => (
|
|
109
|
-
<li key={country.id}>{country.name}</li>
|
|
110
|
-
))}
|
|
111
|
-
</ul>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Authentication Flow
|
|
119
|
-
|
|
120
|
-
### Complete Registration & Login Flow
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
import {
|
|
124
|
-
|
|
125
|
-
// Initialize without auth (for registration)
|
|
126
|
-
const publicClient = new
|
|
127
|
-
apiKey: 'public_api_key' // Or omit for public endpoints
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
async function registerAndLogin() {
|
|
131
|
-
try {
|
|
132
|
-
// 1. Register
|
|
133
|
-
const registration = await publicClient.auth.register({
|
|
134
|
-
email: 'user@example.com',
|
|
135
|
-
password: 'SecurePassword123!',
|
|
136
|
-
username: 'johndoe'
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
console.log('User registered:', registration.user);
|
|
140
|
-
console.log('Access token:', registration.access_token);
|
|
141
|
-
|
|
142
|
-
// 2. Create new client with token
|
|
143
|
-
const authenticatedClient = new
|
|
144
|
-
token: registration.access_token
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// 3. Verify profile
|
|
148
|
-
const profile = await authenticatedClient.auth.getProfile();
|
|
149
|
-
console.log('Profile:', profile);
|
|
150
|
-
|
|
151
|
-
// 4. Store tokens securely
|
|
152
|
-
localStorage.setItem('access_token', registration.access_token);
|
|
153
|
-
localStorage.setItem('refresh_token', registration.refresh_token);
|
|
154
|
-
|
|
155
|
-
return authenticatedClient;
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (error instanceof
|
|
158
|
-
if (error.statusCode === 409) {
|
|
159
|
-
console.error('Email already exists');
|
|
160
|
-
} else {
|
|
161
|
-
console.error('Registration failed:', error.message);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Token refresh helper
|
|
169
|
-
async function refreshAuthToken(refreshToken: string) {
|
|
170
|
-
const client = new
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
const newTokens = await client.auth.refreshToken({
|
|
174
|
-
refresh_token: refreshToken
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
localStorage.setItem('access_token', newTokens.access_token);
|
|
178
|
-
localStorage.setItem('refresh_token', newTokens.refresh_token);
|
|
179
|
-
|
|
180
|
-
return newTokens.access_token;
|
|
181
|
-
} catch (error) {
|
|
182
|
-
// Refresh failed, redirect to login
|
|
183
|
-
localStorage.clear();
|
|
184
|
-
window.location.href = '/login';
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
---
|
|
190
|
-
|
|
191
|
-
## Geography Queries
|
|
192
|
-
|
|
193
|
-
### Building a Location Selector
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
import {
|
|
197
|
-
|
|
198
|
-
const client = new
|
|
199
|
-
|
|
200
|
-
class LocationSelector {
|
|
201
|
-
async getCountries(): Promise<Country[]> {
|
|
202
|
-
return client.geo.getCountries();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async getStatesForCountry(countryIso2: string): Promise<State[]> {
|
|
206
|
-
return client.geo.getStates(countryIso2);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async getCitiesForState(stateId: number): Promise<City[]> {
|
|
210
|
-
return client.geo.getCities(stateId, 1000); // Get up to 1000 cities
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async searchLocation(query: string) {
|
|
214
|
-
// Search across all types
|
|
215
|
-
const results = await client.geo.search({
|
|
216
|
-
query,
|
|
217
|
-
limit: 10
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
countries: results.data.filter(r => r.type === 'country'),
|
|
222
|
-
states: results.data.filter(r => r.type === 'state'),
|
|
223
|
-
cities: results.data.filter(r => r.type === 'city')
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Usage
|
|
229
|
-
const selector = new LocationSelector();
|
|
230
|
-
|
|
231
|
-
// Cascade dropdowns
|
|
232
|
-
const countries = await selector.getCountries();
|
|
233
|
-
const states = await selector.getStatesForCountry('US');
|
|
234
|
-
const cities = await selector.getCitiesForState(1234);
|
|
235
|
-
|
|
236
|
-
// Search as user types
|
|
237
|
-
const results = await selector.searchLocation('new');
|
|
238
|
-
console.log(results); // { countries: [...], states: [...], cities: [...] }
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Caching Geography Data
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
import {
|
|
245
|
-
|
|
246
|
-
class CachedGeoClient {
|
|
247
|
-
private cache = new Map<string, any>();
|
|
248
|
-
private ttl = 24 * 60 * 60 * 1000; // 24 hours
|
|
249
|
-
|
|
250
|
-
constructor(private client: GeoAPI) {}
|
|
251
|
-
|
|
252
|
-
async getCountries(): Promise<Country[]> {
|
|
253
|
-
const cacheKey = 'countries';
|
|
254
|
-
const cached = this.cache.get(cacheKey);
|
|
255
|
-
|
|
256
|
-
if (cached && Date.now() - cached.timestamp < this.ttl) {
|
|
257
|
-
return cached.data;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const data = await this.client.geo.getCountries();
|
|
261
|
-
this.cache.set(cacheKey, { data, timestamp: Date.now() });
|
|
262
|
-
return data;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async getCountryByIso(iso2: string): Promise<Country> {
|
|
266
|
-
const cacheKey = `country:${iso2}`;
|
|
267
|
-
const cached = this.cache.get(cacheKey);
|
|
268
|
-
|
|
269
|
-
if (cached && Date.now() - cached.timestamp < this.ttl) {
|
|
270
|
-
return cached.data;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const data = await this.client.geo.getCountryByIso(iso2);
|
|
274
|
-
this.cache.set(cacheKey, { data, timestamp: Date.now() });
|
|
275
|
-
return data;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
clearCache() {
|
|
279
|
-
this.cache.clear();
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Usage
|
|
284
|
-
const client = new
|
|
285
|
-
const cachedClient = new CachedGeoClient(client);
|
|
286
|
-
|
|
287
|
-
const countries = await cachedClient.getCountries(); // Fetches from API
|
|
288
|
-
const countriesAgain = await cachedClient.getCountries(); // Returns from cache
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
---
|
|
292
|
-
|
|
293
|
-
## Account Management
|
|
294
|
-
|
|
295
|
-
### Usage Monitoring Dashboard
|
|
296
|
-
|
|
297
|
-
```typescript
|
|
298
|
-
import {
|
|
299
|
-
|
|
300
|
-
const client = new
|
|
301
|
-
|
|
302
|
-
async function buildDashboard() {
|
|
303
|
-
const dashboard = await client.account.getDashboard();
|
|
304
|
-
|
|
305
|
-
const usagePercentage = (
|
|
306
|
-
(dashboard.usage.requestsThisMonth / dashboard.usage.monthlyQuota) * 100
|
|
307
|
-
).toFixed(2);
|
|
308
|
-
|
|
309
|
-
console.log('='.repeat(50));
|
|
310
|
-
console.log('ACCOUNT DASHBOARD');
|
|
311
|
-
console.log('='.repeat(50));
|
|
312
|
-
console.log(`User: ${dashboard.user.username} (${dashboard.user.email})`);
|
|
313
|
-
console.log(`Plan: ${dashboard.subscription.tier.toUpperCase()}`);
|
|
314
|
-
console.log(`Status: ${dashboard.subscription.status}`);
|
|
315
|
-
console.log('');
|
|
316
|
-
console.log('Usage:');
|
|
317
|
-
console.log(` Requests: ${dashboard.usage.requestsThisMonth} / ${dashboard.usage.monthlyQuota}`);
|
|
318
|
-
console.log(` Percentage: ${usagePercentage}%`);
|
|
319
|
-
console.log(` Remaining: ${dashboard.usage.remaining}`);
|
|
320
|
-
console.log('');
|
|
321
|
-
console.log('Limits:');
|
|
322
|
-
console.log(` Rate Limit: ${dashboard.limits.rateLimit} req/min`);
|
|
323
|
-
console.log(` Max API Keys: ${dashboard.limits.maxApiKeys}`);
|
|
324
|
-
console.log(` Overage Allowed: ${dashboard.limits.overageAllowed ? 'Yes' : 'No'}`);
|
|
325
|
-
console.log(` Price: $${dashboard.limits.price}/month`);
|
|
326
|
-
console.log('');
|
|
327
|
-
console.log(`API Keys: ${dashboard.apiKeys.active} / ${dashboard.apiKeys.max}`);
|
|
328
|
-
console.log('='.repeat(50));
|
|
329
|
-
|
|
330
|
-
// Alert if quota > 80%
|
|
331
|
-
if (parseFloat(usagePercentage) > 80) {
|
|
332
|
-
console.warn('⚠️ WARNING: You have used more than 80% of your quota!');
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return dashboard;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
buildDashboard();
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
---
|
|
342
|
-
|
|
343
|
-
## API Keys Lifecycle
|
|
344
|
-
|
|
345
|
-
### Rotating API Keys
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
import {
|
|
349
|
-
|
|
350
|
-
const client = new
|
|
351
|
-
|
|
352
|
-
async function rotateApiKey(oldKeyId: string) {
|
|
353
|
-
// 1. Create new key
|
|
354
|
-
const newKey = await client.apiKeys.create({
|
|
355
|
-
name: 'Production Key (Rotated)',
|
|
356
|
-
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
console.log('🔑 New API key created:', newKey.key);
|
|
360
|
-
console.log('⚠️ Update this in your application NOW!');
|
|
361
|
-
|
|
362
|
-
// 2. Wait for confirmation
|
|
363
|
-
console.log('Press Enter after updating your application...');
|
|
364
|
-
await waitForEnter();
|
|
365
|
-
|
|
366
|
-
// 3. Revoke old key
|
|
367
|
-
await client.apiKeys.revoke(oldKeyId);
|
|
368
|
-
console.log('✅ Old key revoked');
|
|
369
|
-
|
|
370
|
-
// 4. Optional: Delete old key after grace period
|
|
371
|
-
setTimeout(async () => {
|
|
372
|
-
await client.apiKeys.delete(oldKeyId);
|
|
373
|
-
console.log('🗑️ Old key deleted');
|
|
374
|
-
}, 7 * 24 * 60 * 60 * 1000); // 7 days grace period
|
|
375
|
-
|
|
376
|
-
return newKey.key;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function waitForEnter(): Promise<void> {
|
|
380
|
-
return new Promise(resolve => {
|
|
381
|
-
process.stdin.once('data', () => resolve());
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
---
|
|
387
|
-
|
|
388
|
-
## Subscription Management
|
|
389
|
-
|
|
390
|
-
### Complete Upgrade Flow
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
import {
|
|
394
|
-
|
|
395
|
-
const client = new
|
|
396
|
-
|
|
397
|
-
async function upgradeSubscription() {
|
|
398
|
-
// 1. Check current tier
|
|
399
|
-
const dashboard = await client.account.getDashboard();
|
|
400
|
-
console.log('Current tier:', dashboard.subscription.tier);
|
|
401
|
-
|
|
402
|
-
// 2. Create checkout session
|
|
403
|
-
const checkout = await client.billing.createCheckoutSession(
|
|
404
|
-
'professional',
|
|
405
|
-
'https://myapp.com/upgrade/success',
|
|
406
|
-
'https://myapp.com/upgrade/cancel'
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
console.log('Checkout URL:', checkout.url);
|
|
410
|
-
console.log('Session ID:', checkout.sessionId);
|
|
411
|
-
|
|
412
|
-
// 3. Redirect user (in browser)
|
|
413
|
-
// window.location.href = checkout.url;
|
|
414
|
-
|
|
415
|
-
// 4. After successful payment, Stripe webhook will update subscription
|
|
416
|
-
// You can then verify:
|
|
417
|
-
setTimeout(async () => {
|
|
418
|
-
const updatedDashboard = await client.account.getDashboard();
|
|
419
|
-
console.log('New tier:', updatedDashboard.subscription.tier);
|
|
420
|
-
}, 5000);
|
|
421
|
-
}
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
---
|
|
425
|
-
|
|
426
|
-
## Webhook Setup
|
|
427
|
-
|
|
428
|
-
### Complete Webhook Implementation
|
|
429
|
-
|
|
430
|
-
```typescript
|
|
431
|
-
import {
|
|
432
|
-
import express from 'express';
|
|
433
|
-
import crypto from 'crypto';
|
|
434
|
-
|
|
435
|
-
const client = new
|
|
436
|
-
const app = express();
|
|
437
|
-
|
|
438
|
-
// 1. Create webhook
|
|
439
|
-
async function setupWebhook() {
|
|
440
|
-
const webhook = await client.webhooks.create({
|
|
441
|
-
url: 'https://myapp.com/webhooks/geo-api',
|
|
442
|
-
events: [
|
|
443
|
-
'usage.quota_warning',
|
|
444
|
-
'usage.quota_exceeded',
|
|
445
|
-
'subscription.updated',
|
|
446
|
-
'subscription.canceled'
|
|
447
|
-
]
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
console.log('Webhook ID:', webhook.id);
|
|
451
|
-
console.log('Secret:', webhook.secret);
|
|
452
|
-
|
|
453
|
-
// Store secret securely
|
|
454
|
-
process.env.WEBHOOK_SECRET = webhook.secret;
|
|
455
|
-
|
|
456
|
-
return webhook;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// 2. Validate webhook signature
|
|
460
|
-
function validateWebhookSignature(
|
|
461
|
-
payload: string,
|
|
462
|
-
signature: string,
|
|
463
|
-
secret: string
|
|
464
|
-
): boolean {
|
|
465
|
-
const expectedSignature = crypto
|
|
466
|
-
.createHmac('sha256', secret)
|
|
467
|
-
.update(payload)
|
|
468
|
-
.digest('hex');
|
|
469
|
-
|
|
470
|
-
return crypto.timingSafeEqual(
|
|
471
|
-
Buffer.from(signature),
|
|
472
|
-
Buffer.from(expectedSignature)
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// 3. Handle webhook events
|
|
477
|
-
app.post('/webhooks/geo-api', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
478
|
-
const signature = req.headers['x-webhook-signature'] as string;
|
|
479
|
-
const payload = req.body.toString();
|
|
480
|
-
|
|
481
|
-
// Validate signature
|
|
482
|
-
if (!validateWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
|
|
483
|
-
return res.status(401).send('Invalid signature');
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Parse event
|
|
487
|
-
const event = JSON.parse(payload);
|
|
488
|
-
|
|
489
|
-
console.log('Webhook event:', event.type);
|
|
490
|
-
|
|
491
|
-
// Handle different events
|
|
492
|
-
switch (event.type) {
|
|
493
|
-
case 'usage.quota_warning':
|
|
494
|
-
console.log(`⚠️ Quota warning: ${event.data.usagePercentage}%`);
|
|
495
|
-
// Send notification to user
|
|
496
|
-
break;
|
|
497
|
-
|
|
498
|
-
case 'usage.quota_exceeded':
|
|
499
|
-
console.log('🚫 Quota exceeded!');
|
|
500
|
-
// Disable features or notify urgently
|
|
501
|
-
break;
|
|
502
|
-
|
|
503
|
-
case 'subscription.updated':
|
|
504
|
-
console.log(`✅ Subscription updated to: ${event.data.tier}`);
|
|
505
|
-
// Update user permissions
|
|
506
|
-
break;
|
|
507
|
-
|
|
508
|
-
case 'subscription.canceled':
|
|
509
|
-
console.log('❌ Subscription canceled');
|
|
510
|
-
// Downgrade user to free tier
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
res.status(200).send('OK');
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
// 4. Test webhook
|
|
518
|
-
async function testWebhook(webhookId: string) {
|
|
519
|
-
const result = await client.webhooks.test(webhookId);
|
|
520
|
-
console.log('Test result:', result.success);
|
|
521
|
-
}
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
---
|
|
525
|
-
|
|
526
|
-
## Error Handling Patterns
|
|
527
|
-
|
|
528
|
-
### Comprehensive Error Handler
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
import {
|
|
532
|
-
|
|
533
|
-
const client = new
|
|
534
|
-
|
|
535
|
-
async function robustApiCall<T>(
|
|
536
|
-
operation: () => Promise<T>,
|
|
537
|
-
retries = 3
|
|
538
|
-
): Promise<T> {
|
|
539
|
-
for (let i = 0; i < retries; i++) {
|
|
540
|
-
try {
|
|
541
|
-
return await operation();
|
|
542
|
-
} catch (error) {
|
|
543
|
-
if (error instanceof
|
|
544
|
-
// Handle specific error codes
|
|
545
|
-
switch (error.statusCode) {
|
|
546
|
-
case 401:
|
|
547
|
-
console.error('Unauthorized - check API key');
|
|
548
|
-
throw error;
|
|
549
|
-
|
|
550
|
-
case 403:
|
|
551
|
-
console.error('Forbidden - quota exceeded or insufficient permissions');
|
|
552
|
-
throw error;
|
|
553
|
-
|
|
554
|
-
case 404:
|
|
555
|
-
console.error('Not found');
|
|
556
|
-
throw error;
|
|
557
|
-
|
|
558
|
-
case 429:
|
|
559
|
-
console.warn(`Rate limited, retrying after delay (attempt ${i + 1})`);
|
|
560
|
-
const retryAfter = error.response?.retryAfter || 1000;
|
|
561
|
-
await new Promise(resolve => setTimeout(resolve, retryAfter));
|
|
562
|
-
continue;
|
|
563
|
-
|
|
564
|
-
case 500:
|
|
565
|
-
case 502:
|
|
566
|
-
case 503:
|
|
567
|
-
console.warn(`Server error, retrying (attempt ${i + 1})`);
|
|
568
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
569
|
-
continue;
|
|
570
|
-
|
|
571
|
-
default:
|
|
572
|
-
console.error('API Error:', error.message);
|
|
573
|
-
throw error;
|
|
574
|
-
}
|
|
575
|
-
} else {
|
|
576
|
-
console.error('Unexpected error:', error);
|
|
577
|
-
throw error;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
throw new Error('Max retries exceeded');
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Usage
|
|
586
|
-
const countries = await robustApiCall(() => client.geo.getCountries());
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
---
|
|
590
|
-
|
|
591
|
-
## Production Best Practices
|
|
592
|
-
|
|
593
|
-
### Singleton Client Pattern
|
|
594
|
-
|
|
595
|
-
```typescript
|
|
596
|
-
// lib/geo-client.ts
|
|
597
|
-
import {
|
|
598
|
-
|
|
599
|
-
let clientInstance: GeoAPI | null = null;
|
|
600
|
-
|
|
601
|
-
export function getGeoClient(): GeoAPI {
|
|
602
|
-
if (!clientInstance) {
|
|
603
|
-
if (!process.env.GEO_API_KEY) {
|
|
604
|
-
throw new Error('GEO_API_KEY environment variable is required');
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
clientInstance = new
|
|
608
|
-
apiKey: process.env.GEO_API_KEY,
|
|
609
|
-
baseURL: process.env.GEO_API_URL,
|
|
610
|
-
timeout: 30000,
|
|
611
|
-
retries: 3,
|
|
612
|
-
debug: process.env.NODE_ENV === 'development'
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return clientInstance;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Usage across your application
|
|
620
|
-
import { getGeoClient } from './lib/geo-client';
|
|
621
|
-
|
|
622
|
-
const client = getGeoClient();
|
|
623
|
-
const countries = await client.geo.getCountries();
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
### Environment-Specific Configuration
|
|
627
|
-
|
|
628
|
-
```typescript
|
|
629
|
-
// config/geo-api.config.ts
|
|
630
|
-
import { SDKConfig } from '
|
|
631
|
-
|
|
632
|
-
const configs: Record<string, SDKConfig> = {
|
|
633
|
-
development: {
|
|
634
|
-
apiKey: process.env.DEV_GEO_API_KEY!,
|
|
635
|
-
baseURL: 'http://localhost:3000/v1',
|
|
636
|
-
timeout: 60000,
|
|
637
|
-
retries: 1,
|
|
638
|
-
debug: true
|
|
639
|
-
},
|
|
640
|
-
|
|
641
|
-
staging: {
|
|
642
|
-
apiKey: process.env.STAGING_GEO_API_KEY!,
|
|
643
|
-
baseURL: 'https://staging-api.
|
|
644
|
-
timeout: 30000,
|
|
645
|
-
retries: 3,
|
|
646
|
-
debug: false
|
|
647
|
-
},
|
|
648
|
-
|
|
649
|
-
production: {
|
|
650
|
-
apiKey: process.env.PROD_GEO_API_KEY!,
|
|
651
|
-
baseURL: 'https://api.
|
|
652
|
-
timeout: 30000,
|
|
653
|
-
retries: 3,
|
|
654
|
-
debug: false
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
|
|
658
|
-
export function getConfig(): SDKConfig {
|
|
659
|
-
const env = process.env.NODE_ENV || 'development';
|
|
660
|
-
return configs[env] || configs.development;
|
|
661
|
-
}
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
---
|
|
665
|
-
|
|
666
|
-
**For more examples, see the [full documentation](https://api.
|
|
1
|
+
# SDK Usage Examples
|
|
2
|
+
|
|
3
|
+
Complete examples demonstrating how to use the Geo API SDK.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Basic Setup](#basic-setup)
|
|
8
|
+
- [Authentication Flow](#authentication-flow)
|
|
9
|
+
- [Geography Queries](#geography-queries)
|
|
10
|
+
- [Account Management](#account-management)
|
|
11
|
+
- [API Keys Lifecycle](#api-keys-lifecycle)
|
|
12
|
+
- [Subscription Management](#subscription-management)
|
|
13
|
+
- [Webhook Setup](#webhook-setup)
|
|
14
|
+
- [Error Handling Patterns](#error-handling-patterns)
|
|
15
|
+
- [Production Best Practices](#production-best-practices)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Basic Setup
|
|
20
|
+
|
|
21
|
+
### Node.js / Express Application
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { ApogeoAPI } from 'apogeoapi';
|
|
25
|
+
import express from 'express';
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
const client = new ApogeoAPI({
|
|
29
|
+
apiKey: process.env.GEO_API_KEY!,
|
|
30
|
+
baseURL: process.env.GEO_API_URL || 'https://api.apogeoapi.com/v1',
|
|
31
|
+
timeout: 30000,
|
|
32
|
+
retries: 3
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.get('/countries', async (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
const countries = await client.geo.getCountries();
|
|
38
|
+
res.json(countries);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
res.status(500).json({ error: error.message });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.listen(3000);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Next.js API Route
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// pages/api/countries.ts
|
|
51
|
+
import { ApogeoAPI } from 'apogeoapi';
|
|
52
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
53
|
+
|
|
54
|
+
const client = new ApogeoAPI({
|
|
55
|
+
apiKey: process.env.GEO_API_KEY!
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export default async function handler(
|
|
59
|
+
req: NextApiRequest,
|
|
60
|
+
res: NextApiResponse
|
|
61
|
+
) {
|
|
62
|
+
try {
|
|
63
|
+
const countries = await client.geo.getCountries();
|
|
64
|
+
res.status(200).json(countries);
|
|
65
|
+
} catch (error: any) {
|
|
66
|
+
res.status(error.statusCode || 500).json({
|
|
67
|
+
error: error.message
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### React Hook
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// hooks/useGeoAPI.ts
|
|
77
|
+
import { ApogeoAPI } from 'apogeoapi';
|
|
78
|
+
import { useState, useEffect } from 'react';
|
|
79
|
+
|
|
80
|
+
const client = new ApogeoAPI({
|
|
81
|
+
apiKey: process.env.NEXT_PUBLIC_GEO_API_KEY!
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export function useCountries() {
|
|
85
|
+
const [countries, setCountries] = useState([]);
|
|
86
|
+
const [loading, setLoading] = useState(true);
|
|
87
|
+
const [error, setError] = useState(null);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
client.geo.getCountries()
|
|
91
|
+
.then(setCountries)
|
|
92
|
+
.catch(setError)
|
|
93
|
+
.finally(() => setLoading(false));
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
return { countries, loading, error };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Usage in component
|
|
100
|
+
function CountriesList() {
|
|
101
|
+
const { countries, loading, error } = useCountries();
|
|
102
|
+
|
|
103
|
+
if (loading) return <div>Loading...</div>;
|
|
104
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<ul>
|
|
108
|
+
{countries.map(country => (
|
|
109
|
+
<li key={country.id}>{country.name}</li>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Authentication Flow
|
|
119
|
+
|
|
120
|
+
### Complete Registration & Login Flow
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { ApogeoAPI, ApogeoAPIError } from 'apogeoapi';
|
|
124
|
+
|
|
125
|
+
// Initialize without auth (for registration)
|
|
126
|
+
const publicClient = new ApogeoAPI({
|
|
127
|
+
apiKey: 'public_api_key' // Or omit for public endpoints
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
async function registerAndLogin() {
|
|
131
|
+
try {
|
|
132
|
+
// 1. Register
|
|
133
|
+
const registration = await publicClient.auth.register({
|
|
134
|
+
email: 'user@example.com',
|
|
135
|
+
password: 'SecurePassword123!',
|
|
136
|
+
username: 'johndoe'
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log('User registered:', registration.user);
|
|
140
|
+
console.log('Access token:', registration.access_token);
|
|
141
|
+
|
|
142
|
+
// 2. Create new client with token
|
|
143
|
+
const authenticatedClient = new ApogeoAPI({
|
|
144
|
+
token: registration.access_token
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 3. Verify profile
|
|
148
|
+
const profile = await authenticatedClient.auth.getProfile();
|
|
149
|
+
console.log('Profile:', profile);
|
|
150
|
+
|
|
151
|
+
// 4. Store tokens securely
|
|
152
|
+
localStorage.setItem('access_token', registration.access_token);
|
|
153
|
+
localStorage.setItem('refresh_token', registration.refresh_token);
|
|
154
|
+
|
|
155
|
+
return authenticatedClient;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof ApogeoAPIError) {
|
|
158
|
+
if (error.statusCode === 409) {
|
|
159
|
+
console.error('Email already exists');
|
|
160
|
+
} else {
|
|
161
|
+
console.error('Registration failed:', error.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Token refresh helper
|
|
169
|
+
async function refreshAuthToken(refreshToken: string) {
|
|
170
|
+
const client = new ApogeoAPI({ token: 'temp' });
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const newTokens = await client.auth.refreshToken({
|
|
174
|
+
refresh_token: refreshToken
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
localStorage.setItem('access_token', newTokens.access_token);
|
|
178
|
+
localStorage.setItem('refresh_token', newTokens.refresh_token);
|
|
179
|
+
|
|
180
|
+
return newTokens.access_token;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// Refresh failed, redirect to login
|
|
183
|
+
localStorage.clear();
|
|
184
|
+
window.location.href = '/login';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Geography Queries
|
|
192
|
+
|
|
193
|
+
### Building a Location Selector
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { ApogeoAPI, Country, State, City } from 'apogeoapi';
|
|
197
|
+
|
|
198
|
+
const client = new ApogeoAPI({ apiKey: process.env.GEO_API_KEY! });
|
|
199
|
+
|
|
200
|
+
class LocationSelector {
|
|
201
|
+
async getCountries(): Promise<Country[]> {
|
|
202
|
+
return client.geo.getCountries();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getStatesForCountry(countryIso2: string): Promise<State[]> {
|
|
206
|
+
return client.geo.getStates(countryIso2);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async getCitiesForState(stateId: number): Promise<City[]> {
|
|
210
|
+
return client.geo.getCities(stateId, 1000); // Get up to 1000 cities
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async searchLocation(query: string) {
|
|
214
|
+
// Search across all types
|
|
215
|
+
const results = await client.geo.search({
|
|
216
|
+
query,
|
|
217
|
+
limit: 10
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
countries: results.data.filter(r => r.type === 'country'),
|
|
222
|
+
states: results.data.filter(r => r.type === 'state'),
|
|
223
|
+
cities: results.data.filter(r => r.type === 'city')
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Usage
|
|
229
|
+
const selector = new LocationSelector();
|
|
230
|
+
|
|
231
|
+
// Cascade dropdowns
|
|
232
|
+
const countries = await selector.getCountries();
|
|
233
|
+
const states = await selector.getStatesForCountry('US');
|
|
234
|
+
const cities = await selector.getCitiesForState(1234);
|
|
235
|
+
|
|
236
|
+
// Search as user types
|
|
237
|
+
const results = await selector.searchLocation('new');
|
|
238
|
+
console.log(results); // { countries: [...], states: [...], cities: [...] }
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Caching Geography Data
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { ApogeoAPI, Country } from 'apogeoapi';
|
|
245
|
+
|
|
246
|
+
class CachedGeoClient {
|
|
247
|
+
private cache = new Map<string, any>();
|
|
248
|
+
private ttl = 24 * 60 * 60 * 1000; // 24 hours
|
|
249
|
+
|
|
250
|
+
constructor(private client: GeoAPI) {}
|
|
251
|
+
|
|
252
|
+
async getCountries(): Promise<Country[]> {
|
|
253
|
+
const cacheKey = 'countries';
|
|
254
|
+
const cached = this.cache.get(cacheKey);
|
|
255
|
+
|
|
256
|
+
if (cached && Date.now() - cached.timestamp < this.ttl) {
|
|
257
|
+
return cached.data;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const data = await this.client.geo.getCountries();
|
|
261
|
+
this.cache.set(cacheKey, { data, timestamp: Date.now() });
|
|
262
|
+
return data;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async getCountryByIso(iso2: string): Promise<Country> {
|
|
266
|
+
const cacheKey = `country:${iso2}`;
|
|
267
|
+
const cached = this.cache.get(cacheKey);
|
|
268
|
+
|
|
269
|
+
if (cached && Date.now() - cached.timestamp < this.ttl) {
|
|
270
|
+
return cached.data;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const data = await this.client.geo.getCountryByIso(iso2);
|
|
274
|
+
this.cache.set(cacheKey, { data, timestamp: Date.now() });
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
clearCache() {
|
|
279
|
+
this.cache.clear();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Usage
|
|
284
|
+
const client = new ApogeoAPI({ apiKey: 'xxx' });
|
|
285
|
+
const cachedClient = new CachedGeoClient(client);
|
|
286
|
+
|
|
287
|
+
const countries = await cachedClient.getCountries(); // Fetches from API
|
|
288
|
+
const countriesAgain = await cachedClient.getCountries(); // Returns from cache
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Account Management
|
|
294
|
+
|
|
295
|
+
### Usage Monitoring Dashboard
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { ApogeoAPI } from 'apogeoapi';
|
|
299
|
+
|
|
300
|
+
const client = new ApogeoAPI({ apiKey: 'xxx' });
|
|
301
|
+
|
|
302
|
+
async function buildDashboard() {
|
|
303
|
+
const dashboard = await client.account.getDashboard();
|
|
304
|
+
|
|
305
|
+
const usagePercentage = (
|
|
306
|
+
(dashboard.usage.requestsThisMonth / dashboard.usage.monthlyQuota) * 100
|
|
307
|
+
).toFixed(2);
|
|
308
|
+
|
|
309
|
+
console.log('='.repeat(50));
|
|
310
|
+
console.log('ACCOUNT DASHBOARD');
|
|
311
|
+
console.log('='.repeat(50));
|
|
312
|
+
console.log(`User: ${dashboard.user.username} (${dashboard.user.email})`);
|
|
313
|
+
console.log(`Plan: ${dashboard.subscription.tier.toUpperCase()}`);
|
|
314
|
+
console.log(`Status: ${dashboard.subscription.status}`);
|
|
315
|
+
console.log('');
|
|
316
|
+
console.log('Usage:');
|
|
317
|
+
console.log(` Requests: ${dashboard.usage.requestsThisMonth} / ${dashboard.usage.monthlyQuota}`);
|
|
318
|
+
console.log(` Percentage: ${usagePercentage}%`);
|
|
319
|
+
console.log(` Remaining: ${dashboard.usage.remaining}`);
|
|
320
|
+
console.log('');
|
|
321
|
+
console.log('Limits:');
|
|
322
|
+
console.log(` Rate Limit: ${dashboard.limits.rateLimit} req/min`);
|
|
323
|
+
console.log(` Max API Keys: ${dashboard.limits.maxApiKeys}`);
|
|
324
|
+
console.log(` Overage Allowed: ${dashboard.limits.overageAllowed ? 'Yes' : 'No'}`);
|
|
325
|
+
console.log(` Price: $${dashboard.limits.price}/month`);
|
|
326
|
+
console.log('');
|
|
327
|
+
console.log(`API Keys: ${dashboard.apiKeys.active} / ${dashboard.apiKeys.max}`);
|
|
328
|
+
console.log('='.repeat(50));
|
|
329
|
+
|
|
330
|
+
// Alert if quota > 80%
|
|
331
|
+
if (parseFloat(usagePercentage) > 80) {
|
|
332
|
+
console.warn('⚠️ WARNING: You have used more than 80% of your quota!');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return dashboard;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
buildDashboard();
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## API Keys Lifecycle
|
|
344
|
+
|
|
345
|
+
### Rotating API Keys
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { ApogeoAPI } from 'apogeoapi';
|
|
349
|
+
|
|
350
|
+
const client = new ApogeoAPI({ token: 'user_jwt_token' });
|
|
351
|
+
|
|
352
|
+
async function rotateApiKey(oldKeyId: string) {
|
|
353
|
+
// 1. Create new key
|
|
354
|
+
const newKey = await client.apiKeys.create({
|
|
355
|
+
name: 'Production Key (Rotated)',
|
|
356
|
+
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
console.log('🔑 New API key created:', newKey.key);
|
|
360
|
+
console.log('⚠️ Update this in your application NOW!');
|
|
361
|
+
|
|
362
|
+
// 2. Wait for confirmation
|
|
363
|
+
console.log('Press Enter after updating your application...');
|
|
364
|
+
await waitForEnter();
|
|
365
|
+
|
|
366
|
+
// 3. Revoke old key
|
|
367
|
+
await client.apiKeys.revoke(oldKeyId);
|
|
368
|
+
console.log('✅ Old key revoked');
|
|
369
|
+
|
|
370
|
+
// 4. Optional: Delete old key after grace period
|
|
371
|
+
setTimeout(async () => {
|
|
372
|
+
await client.apiKeys.delete(oldKeyId);
|
|
373
|
+
console.log('🗑️ Old key deleted');
|
|
374
|
+
}, 7 * 24 * 60 * 60 * 1000); // 7 days grace period
|
|
375
|
+
|
|
376
|
+
return newKey.key;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function waitForEnter(): Promise<void> {
|
|
380
|
+
return new Promise(resolve => {
|
|
381
|
+
process.stdin.once('data', () => resolve());
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Subscription Management
|
|
389
|
+
|
|
390
|
+
### Complete Upgrade Flow
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import { ApogeoAPI } from 'apogeoapi';
|
|
394
|
+
|
|
395
|
+
const client = new ApogeoAPI({ token: 'user_jwt_token' });
|
|
396
|
+
|
|
397
|
+
async function upgradeSubscription() {
|
|
398
|
+
// 1. Check current tier
|
|
399
|
+
const dashboard = await client.account.getDashboard();
|
|
400
|
+
console.log('Current tier:', dashboard.subscription.tier);
|
|
401
|
+
|
|
402
|
+
// 2. Create checkout session
|
|
403
|
+
const checkout = await client.billing.createCheckoutSession(
|
|
404
|
+
'professional',
|
|
405
|
+
'https://myapp.com/upgrade/success',
|
|
406
|
+
'https://myapp.com/upgrade/cancel'
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
console.log('Checkout URL:', checkout.url);
|
|
410
|
+
console.log('Session ID:', checkout.sessionId);
|
|
411
|
+
|
|
412
|
+
// 3. Redirect user (in browser)
|
|
413
|
+
// window.location.href = checkout.url;
|
|
414
|
+
|
|
415
|
+
// 4. After successful payment, Stripe webhook will update subscription
|
|
416
|
+
// You can then verify:
|
|
417
|
+
setTimeout(async () => {
|
|
418
|
+
const updatedDashboard = await client.account.getDashboard();
|
|
419
|
+
console.log('New tier:', updatedDashboard.subscription.tier);
|
|
420
|
+
}, 5000);
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Webhook Setup
|
|
427
|
+
|
|
428
|
+
### Complete Webhook Implementation
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { ApogeoAPI } from 'apogeoapi';
|
|
432
|
+
import express from 'express';
|
|
433
|
+
import crypto from 'crypto';
|
|
434
|
+
|
|
435
|
+
const client = new ApogeoAPI({ token: 'user_jwt_token' });
|
|
436
|
+
const app = express();
|
|
437
|
+
|
|
438
|
+
// 1. Create webhook
|
|
439
|
+
async function setupWebhook() {
|
|
440
|
+
const webhook = await client.webhooks.create({
|
|
441
|
+
url: 'https://myapp.com/webhooks/geo-api',
|
|
442
|
+
events: [
|
|
443
|
+
'usage.quota_warning',
|
|
444
|
+
'usage.quota_exceeded',
|
|
445
|
+
'subscription.updated',
|
|
446
|
+
'subscription.canceled'
|
|
447
|
+
]
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
console.log('Webhook ID:', webhook.id);
|
|
451
|
+
console.log('Secret:', webhook.secret);
|
|
452
|
+
|
|
453
|
+
// Store secret securely
|
|
454
|
+
process.env.WEBHOOK_SECRET = webhook.secret;
|
|
455
|
+
|
|
456
|
+
return webhook;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 2. Validate webhook signature
|
|
460
|
+
function validateWebhookSignature(
|
|
461
|
+
payload: string,
|
|
462
|
+
signature: string,
|
|
463
|
+
secret: string
|
|
464
|
+
): boolean {
|
|
465
|
+
const expectedSignature = crypto
|
|
466
|
+
.createHmac('sha256', secret)
|
|
467
|
+
.update(payload)
|
|
468
|
+
.digest('hex');
|
|
469
|
+
|
|
470
|
+
return crypto.timingSafeEqual(
|
|
471
|
+
Buffer.from(signature),
|
|
472
|
+
Buffer.from(expectedSignature)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 3. Handle webhook events
|
|
477
|
+
app.post('/webhooks/geo-api', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
478
|
+
const signature = req.headers['x-webhook-signature'] as string;
|
|
479
|
+
const payload = req.body.toString();
|
|
480
|
+
|
|
481
|
+
// Validate signature
|
|
482
|
+
if (!validateWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
|
|
483
|
+
return res.status(401).send('Invalid signature');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Parse event
|
|
487
|
+
const event = JSON.parse(payload);
|
|
488
|
+
|
|
489
|
+
console.log('Webhook event:', event.type);
|
|
490
|
+
|
|
491
|
+
// Handle different events
|
|
492
|
+
switch (event.type) {
|
|
493
|
+
case 'usage.quota_warning':
|
|
494
|
+
console.log(`⚠️ Quota warning: ${event.data.usagePercentage}%`);
|
|
495
|
+
// Send notification to user
|
|
496
|
+
break;
|
|
497
|
+
|
|
498
|
+
case 'usage.quota_exceeded':
|
|
499
|
+
console.log('🚫 Quota exceeded!');
|
|
500
|
+
// Disable features or notify urgently
|
|
501
|
+
break;
|
|
502
|
+
|
|
503
|
+
case 'subscription.updated':
|
|
504
|
+
console.log(`✅ Subscription updated to: ${event.data.tier}`);
|
|
505
|
+
// Update user permissions
|
|
506
|
+
break;
|
|
507
|
+
|
|
508
|
+
case 'subscription.canceled':
|
|
509
|
+
console.log('❌ Subscription canceled');
|
|
510
|
+
// Downgrade user to free tier
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
res.status(200).send('OK');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// 4. Test webhook
|
|
518
|
+
async function testWebhook(webhookId: string) {
|
|
519
|
+
const result = await client.webhooks.test(webhookId);
|
|
520
|
+
console.log('Test result:', result.success);
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Error Handling Patterns
|
|
527
|
+
|
|
528
|
+
### Comprehensive Error Handler
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { ApogeoAPI, ApogeoAPIError } from 'apogeoapi';
|
|
532
|
+
|
|
533
|
+
const client = new ApogeoAPI({ apiKey: 'xxx' });
|
|
534
|
+
|
|
535
|
+
async function robustApiCall<T>(
|
|
536
|
+
operation: () => Promise<T>,
|
|
537
|
+
retries = 3
|
|
538
|
+
): Promise<T> {
|
|
539
|
+
for (let i = 0; i < retries; i++) {
|
|
540
|
+
try {
|
|
541
|
+
return await operation();
|
|
542
|
+
} catch (error) {
|
|
543
|
+
if (error instanceof ApogeoAPIError) {
|
|
544
|
+
// Handle specific error codes
|
|
545
|
+
switch (error.statusCode) {
|
|
546
|
+
case 401:
|
|
547
|
+
console.error('Unauthorized - check API key');
|
|
548
|
+
throw error;
|
|
549
|
+
|
|
550
|
+
case 403:
|
|
551
|
+
console.error('Forbidden - quota exceeded or insufficient permissions');
|
|
552
|
+
throw error;
|
|
553
|
+
|
|
554
|
+
case 404:
|
|
555
|
+
console.error('Not found');
|
|
556
|
+
throw error;
|
|
557
|
+
|
|
558
|
+
case 429:
|
|
559
|
+
console.warn(`Rate limited, retrying after delay (attempt ${i + 1})`);
|
|
560
|
+
const retryAfter = error.response?.retryAfter || 1000;
|
|
561
|
+
await new Promise(resolve => setTimeout(resolve, retryAfter));
|
|
562
|
+
continue;
|
|
563
|
+
|
|
564
|
+
case 500:
|
|
565
|
+
case 502:
|
|
566
|
+
case 503:
|
|
567
|
+
console.warn(`Server error, retrying (attempt ${i + 1})`);
|
|
568
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
569
|
+
continue;
|
|
570
|
+
|
|
571
|
+
default:
|
|
572
|
+
console.error('API Error:', error.message);
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
console.error('Unexpected error:', error);
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
throw new Error('Max retries exceeded');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Usage
|
|
586
|
+
const countries = await robustApiCall(() => client.geo.getCountries());
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## Production Best Practices
|
|
592
|
+
|
|
593
|
+
### Singleton Client Pattern
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// lib/geo-client.ts
|
|
597
|
+
import { ApogeoAPI } from 'apogeoapi';
|
|
598
|
+
|
|
599
|
+
let clientInstance: GeoAPI | null = null;
|
|
600
|
+
|
|
601
|
+
export function getGeoClient(): GeoAPI {
|
|
602
|
+
if (!clientInstance) {
|
|
603
|
+
if (!process.env.GEO_API_KEY) {
|
|
604
|
+
throw new Error('GEO_API_KEY environment variable is required');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
clientInstance = new ApogeoAPI({
|
|
608
|
+
apiKey: process.env.GEO_API_KEY,
|
|
609
|
+
baseURL: process.env.GEO_API_URL,
|
|
610
|
+
timeout: 30000,
|
|
611
|
+
retries: 3,
|
|
612
|
+
debug: process.env.NODE_ENV === 'development'
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return clientInstance;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Usage across your application
|
|
620
|
+
import { getGeoClient } from './lib/geo-client';
|
|
621
|
+
|
|
622
|
+
const client = getGeoClient();
|
|
623
|
+
const countries = await client.geo.getCountries();
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Environment-Specific Configuration
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
// config/geo-api.config.ts
|
|
630
|
+
import { SDKConfig } from 'apogeoapi';
|
|
631
|
+
|
|
632
|
+
const configs: Record<string, SDKConfig> = {
|
|
633
|
+
development: {
|
|
634
|
+
apiKey: process.env.DEV_GEO_API_KEY!,
|
|
635
|
+
baseURL: 'http://localhost:3000/v1',
|
|
636
|
+
timeout: 60000,
|
|
637
|
+
retries: 1,
|
|
638
|
+
debug: true
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
staging: {
|
|
642
|
+
apiKey: process.env.STAGING_GEO_API_KEY!,
|
|
643
|
+
baseURL: 'https://staging-api.apogeoapi.com/v1',
|
|
644
|
+
timeout: 30000,
|
|
645
|
+
retries: 3,
|
|
646
|
+
debug: false
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
production: {
|
|
650
|
+
apiKey: process.env.PROD_GEO_API_KEY!,
|
|
651
|
+
baseURL: 'https://api.apogeoapi.com/v1',
|
|
652
|
+
timeout: 30000,
|
|
653
|
+
retries: 3,
|
|
654
|
+
debug: false
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
export function getConfig(): SDKConfig {
|
|
659
|
+
const env = process.env.NODE_ENV || 'development';
|
|
660
|
+
return configs[env] || configs.development;
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
**For more examples, see the [full documentation](https://api.apogeoapi.com/api/docs).**
|