apogeoapi 1.0.0
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 +52 -0
- package/EXAMPLES.md +666 -0
- package/README.md +532 -0
- package/dist/clients/account.client.d.ts +76 -0
- package/dist/clients/account.client.d.ts.map +1 -0
- package/dist/clients/account.client.js +85 -0
- package/dist/clients/account.client.js.map +1 -0
- package/dist/clients/api-keys.client.d.ts +90 -0
- package/dist/clients/api-keys.client.d.ts.map +1 -0
- package/dist/clients/api-keys.client.js +105 -0
- package/dist/clients/api-keys.client.js.map +1 -0
- package/dist/clients/auth.client.d.ts +63 -0
- package/dist/clients/auth.client.d.ts.map +1 -0
- package/dist/clients/auth.client.js +86 -0
- package/dist/clients/auth.client.js.map +1 -0
- package/dist/clients/billing.client.d.ts +79 -0
- package/dist/clients/billing.client.d.ts.map +1 -0
- package/dist/clients/billing.client.js +101 -0
- package/dist/clients/billing.client.js.map +1 -0
- package/dist/clients/geo.client.d.ts +105 -0
- package/dist/clients/geo.client.d.ts.map +1 -0
- package/dist/clients/geo.client.js +149 -0
- package/dist/clients/geo.client.js.map +1 -0
- package/dist/clients/webhooks.client.d.ts +110 -0
- package/dist/clients/webhooks.client.d.ts.map +1 -0
- package/dist/clients/webhooks.client.js +129 -0
- package/dist/clients/webhooks.client.js.map +1 -0
- package/dist/index.d.ts +137 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +157 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +243 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +16 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/http-client.d.ts +43 -0
- package/dist/utils/http-client.d.ts.map +1 -0
- package/dist/utils/http-client.js +140 -0
- package/dist/utils/http-client.js.map +1 -0
- package/package.json +44 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the @geo-api/sdk will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2024-01-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of @geo-api/sdk
|
|
12
|
+
- Full TypeScript support with complete type definitions
|
|
13
|
+
- **AuthClient** - Authentication (register, login, refresh token)
|
|
14
|
+
- **GeoClient** - Geography data (countries, states, cities, search)
|
|
15
|
+
- **AccountClient** - Account management (dashboard, profile, usage stats)
|
|
16
|
+
- **ApiKeysClient** - API keys CRUD operations
|
|
17
|
+
- **BillingClient** - Subscription and billing management
|
|
18
|
+
- **WebhooksClient** - Webhook configuration and logs
|
|
19
|
+
- Auto-retry logic with exponential backoff
|
|
20
|
+
- Rate limit handling with retry-after support
|
|
21
|
+
- Comprehensive error handling with typed errors
|
|
22
|
+
- Request/Response interceptors
|
|
23
|
+
- Configurable timeout and retry settings
|
|
24
|
+
- Debug mode for development
|
|
25
|
+
- Complete JSDoc documentation
|
|
26
|
+
- Extensive README with examples
|
|
27
|
+
- Example usage patterns document
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
- ✅ Automatic token management
|
|
31
|
+
- ✅ Type-safe API with full autocomplete
|
|
32
|
+
- ✅ Zero configuration required (sensible defaults)
|
|
33
|
+
- ✅ Works in Node.js and browser environments
|
|
34
|
+
- ✅ CommonJS and ESM support
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
- Complete README with quick start guide
|
|
38
|
+
- API reference for all clients
|
|
39
|
+
- Usage examples for common scenarios
|
|
40
|
+
- Error handling patterns
|
|
41
|
+
- Production best practices
|
|
42
|
+
|
|
43
|
+
## [Unreleased]
|
|
44
|
+
|
|
45
|
+
### Planned
|
|
46
|
+
- [ ] WebSocket support for real-time updates
|
|
47
|
+
- [ ] Batch operations support
|
|
48
|
+
- [ ] Response caching layer
|
|
49
|
+
- [ ] Request cancellation support
|
|
50
|
+
- [ ] GraphQL client (if API adds GraphQL)
|
|
51
|
+
- [ ] React hooks package (@geo-api/react)
|
|
52
|
+
- [ ] Vue composables package (@geo-api/vue)
|
package/EXAMPLES.md
ADDED
|
@@ -0,0 +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 { GeoAPI } from '@geo-api/sdk';
|
|
25
|
+
import express from 'express';
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
const client = new GeoAPI({
|
|
29
|
+
apiKey: process.env.GEO_API_KEY!,
|
|
30
|
+
baseURL: process.env.GEO_API_URL || 'https://api.yourcompany.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 { GeoAPI } from '@geo-api/sdk';
|
|
52
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
53
|
+
|
|
54
|
+
const client = new GeoAPI({
|
|
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 { GeoAPI } from '@geo-api/sdk';
|
|
78
|
+
import { useState, useEffect } from 'react';
|
|
79
|
+
|
|
80
|
+
const client = new GeoAPI({
|
|
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 { GeoAPI, GeoAPIError } from '@geo-api/sdk';
|
|
124
|
+
|
|
125
|
+
// Initialize without auth (for registration)
|
|
126
|
+
const publicClient = new GeoAPI({
|
|
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 GeoAPI({
|
|
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 GeoAPIError) {
|
|
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 GeoAPI({ 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 { GeoAPI, Country, State, City } from '@geo-api/sdk';
|
|
197
|
+
|
|
198
|
+
const client = new GeoAPI({ 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 { GeoAPI, Country } from '@geo-api/sdk';
|
|
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 GeoAPI({ 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 { GeoAPI } from '@geo-api/sdk';
|
|
299
|
+
|
|
300
|
+
const client = new GeoAPI({ 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 { GeoAPI } from '@geo-api/sdk';
|
|
349
|
+
|
|
350
|
+
const client = new GeoAPI({ 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 { GeoAPI } from '@geo-api/sdk';
|
|
394
|
+
|
|
395
|
+
const client = new GeoAPI({ 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 { GeoAPI } from '@geo-api/sdk';
|
|
432
|
+
import express from 'express';
|
|
433
|
+
import crypto from 'crypto';
|
|
434
|
+
|
|
435
|
+
const client = new GeoAPI({ 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 { GeoAPI, GeoAPIError } from '@geo-api/sdk';
|
|
532
|
+
|
|
533
|
+
const client = new GeoAPI({ 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 GeoAPIError) {
|
|
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 { GeoAPI } from '@geo-api/sdk';
|
|
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 GeoAPI({
|
|
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 '@geo-api/sdk';
|
|
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.yourcompany.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.yourcompany.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.yourcompany.com/docs).**
|