API Integration Prompt Templates

AI prompt templates for API integrations. Connect and work with external services and APIs.

Overview

Integrating third-party APIs connects your application to external services, payments, email, analytics, and more. These prompts help you build solid integrations that handle authentication, rate limits, errors, and data transformation. The goal is integrations that work reliably in production, not just in happy-path testing.

Best Practices

1

Specify the API you're integrating (with version if relevant) for accurate implementation details

2

Include your authentication method and any SDK/library preferences

3

Describe your error handling requirements, retries, fallbacks, user notifications

4

Mention rate limits and how you plan to handle them

5

Include data transformation needs, how API data maps to your domain models

Prompt Templates

1. API Client Implementation

Create an API client for this service:

**API:**
- Service: [API NAME]
- Base URL: [ENDPOINT]
- Documentation: [LINK IF AVAILABLE]

**Authentication:**
- Method: [API KEY / OAUTH / JWT / etc.]
- Token location: [HEADER / QUERY / BODY]

**Operations needed:**
- [OPERATION 1 - e.g. list users]
- [OPERATION 2 - e.g. create payment]

**Requirements:**
- Language: [LANGUAGE]
- Error handling: [RETRY STRATEGY]
- Rate limiting: [LIMITS IF KNOWN]
- Logging: [WHAT TO LOG]

**Provide:**
1. API client class/module
2. Request/response types
3. Error handling
4. Usage examples
API: Stripe Payments API
Auth: API key in Authorization header (Bearer)
Operations: Create customer, create payment intent, list payments
Language: TypeScript
Error handling: Retry on 5xx and rate limits, throw on 4xx
Rate limiting: 100 requests/second
Logging: Log request/response for debugging
## Stripe API Client

```typescript
// stripe-client.ts
import Stripe from 'stripe';

// Types
export interface CreateCustomerInput {
 email: string;
 name?: string;
 metadata?: Record<string, string>;
}

export interface CreatePaymentIntentInput {
 amount: number; // in cents
 currency: string;
 customerId: string;
 metadata?: Record<string, string>;
}

export interface PaymentListOptions {
 customerId?: string;
 limit?: number;
 startingAfter?: string;
}

// Client configuration
interface StripeClientConfig {
 apiKey: string;
 maxRetries?: number;
 timeout?: number;
 logRequests?: boolean;
}

export class StripeClient {
 private stripe: Stripe;
 private config: Required<StripeClientConfig>;
 private logger: Console;

 constructor(config: StripeClientConfig) {
 this.config = {
 maxRetries: 3,
 timeout: 30000,
 logRequests: false...config,
 };

 this.stripe = new Stripe(config.apiKey, {
 apiVersion: '2023-10-16',
 maxNetworkRetries: this.config.maxRetries,
 timeout: this.config.timeout,
 });

 this.logger = console;
 }

 // ==================== CUSTOMERS ====================

 async createCustomer(input: CreateCustomerInput): Promise<Stripe.Customer> {
 this.logRequest('createCustomer', input);

 try {
 const customer = await this.stripe.customers.create({
 email: input.email,
 name: input.name,
 metadata: input.metadata,
 });

 this.logResponse('createCustomer', customer);
 return customer;
 } catch (error) {
 throw this.handleError('createCustomer', error);
 }
 }

 async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
 this.logRequest('getCustomer', { customerId });

 try {
 const customer = await this.stripe.customers.retrieve(customerId);
 
 if (customer.deleted) {
 return null;
 }
 
 this.logResponse('getCustomer', customer);
 return customer as Stripe.Customer;
 } catch (error) {
 if (this.isNotFoundError(error)) {
 return null;
 }
 throw this.handleError('getCustomer', error);
 }
 }

 // ==================== PAYMENT INTENTS ====================

 async createPaymentIntent(
 input: CreatePaymentIntentInput
 ): Promise<Stripe.PaymentIntent> {
 this.logRequest('createPaymentIntent', input);

 try {
 const paymentIntent = await this.stripe.paymentIntents.create({
 amount: input.amount,
 currency: input.currency,
 customer: input.customerId,
 metadata: input.metadata,
 automatic_payment_methods: {
 enabled: true,
 },
 });

 this.logResponse('createPaymentIntent', paymentIntent);
 return paymentIntent;
 } catch (error) {
 throw this.handleError('createPaymentIntent', error);
 }
 }

 async confirmPaymentIntent(
 paymentIntentId: string,
 paymentMethodId: string
 ): Promise<Stripe.PaymentIntent> {
 this.logRequest('confirmPaymentIntent', { paymentIntentId, paymentMethodId });

 try {
 const paymentIntent = await this.stripe.paymentIntents.confirm(
 paymentIntentId,
 {
 payment_method: paymentMethodId,
 }
 );

 this.logResponse('confirmPaymentIntent', paymentIntent);
 return paymentIntent;
 } catch (error) {
 throw this.handleError('confirmPaymentIntent', error);
 }
 }

 // ==================== PAYMENTS LIST ====================

 async listPayments(
 options: PaymentListOptions = {}
 ): Promise<Stripe.ApiList<Stripe.PaymentIntent>> {
 this.logRequest('listPayments', options);

 try {
 const payments = await this.stripe.paymentIntents.list({
 customer: options.customerId,
 limit: options.limit || 10,
 starting_after: options.startingAfter,
 });

 this.logResponse('listPayments', { count: payments.data.length });
 return payments;
 } catch (error) {
 throw this.handleError('listPayments', error);
 }
 }

 // ==================== ERROR HANDLING ====================

 private handleError(operation: string, error: unknown): StripeApiError {
 if (error instanceof Stripe.errors.StripeError) {
 const apiError = new StripeApiError(
 error.message,
 error.code || 'unknown',
 error.statusCode || 500,
 operation
 );

 this.logger.error('Stripe API error', {
 operation,
 code: error.code,
 statusCode: error.statusCode,
 message: error.message,
 requestId: error.requestId,
 });

 return apiError;
 }

 // Unknown error
 this.logger.error('Unknown Stripe error', { operation, error });
 return new StripeApiError(
 'An unexpected error occurred',
 'unknown_error',
 500,
 operation
 );
 }

 private isNotFoundError(error: unknown): boolean {
 return (
 error instanceof Stripe.errors.StripeError &&
 error.statusCode === 404
 );
 }

 // ==================== LOGGING ====================

 private logRequest(operation: string, params: unknown): void {
 if (this.config.logRequests) {
 this.logger.debug(`Stripe ${operation} request`, {
 operation,
 params: this.sanitizeForLogging(params),
 });
 }
 }

 private logResponse(operation: string, response: unknown): void {
 if (this.config.logRequests) {
 this.logger.debug(`Stripe ${operation} response`, {
 operation,
 response: this.sanitizeForLogging(response),
 });
 }
 }

 private sanitizeForLogging(data: unknown): unknown {
 // Remove sensitive fields from logs
 if (typeof data === 'object' && data !== null) {
 const sanitized = {...data } as Record<string, unknown>;
 delete sanitized.client_secret;
 return sanitized;
 }
 return data;
 }
}

// Custom error class
export class StripeApiError extends Error {
 constructor(
 message: string,
 public code: string,
 public statusCode: number,
 public operation: string
 ) {
 super(message);
 this.name = 'StripeApiError';
 }

 isRetryable(): boolean {
 return this.statusCode >= 500 || this.code === 'rate_limit';
 }
}
```

### Usage Example

```typescript
import { StripeClient, StripeApiError } from './stripe-client';

const stripe = new StripeClient({
 apiKey: process.env.STRIPE_SECRET_KEY!,
 logRequests: process.env.NODE_ENV === 'development',
});

// Create customer and payment
async function processPayment(email: string, amount: number) {
 try {
 // Create or get customer
 const customer = await stripe.createCustomer({ email });

 // Create payment intent
 const paymentIntent = await stripe.createPaymentIntent({
 amount: amount * 100, // Convert to cents
 currency: 'usd',
 customerId: customer.id,
 metadata: { orderId: 'order_123' },
 });

 return {
 clientSecret: paymentIntent.client_secret,
 paymentIntentId: paymentIntent.id,
 };
 } catch (error) {
 if (error instanceof StripeApiError) {
 if (error.code === 'card_declined') {
 throw new PaymentDeclinedError(error.message);
 }
 }
 throw error;
 }
}
```

When integrating with external APIs, building SDK wrappers, or creating reusable API clients

  • Use the official SDK when available, it handles auth, retries, and serialization correctly
  • Wrap SDK errors in your own error types for consistent error handling across your app

2. Webhook Handler

Create a webhook handler for this service:

**Service:** [API NAME]
**Events to handle:**
- [EVENT 1 - e.g. payment.completed]
- [EVENT 2 - e.g. subscription.cancelled]

**Verification:**
- Method: [SIGNATURE / SECRET / NONE]
- Header: [WHERE SIGNATURE IS]

**Requirements:**
- Framework: [EXPRESS / FASTAPI / etc.]
- Idempotency: [HOW TO HANDLE DUPLICATES]
- Error handling: [WHAT HAPPENS ON FAILURE]

**Provide:**
1. Webhook endpoint
2. Signature verification
3. Event routing
4. Idempotency handling
5. Error responses
Service: Stripe
Events: payment_intent.succeeded, payment_intent.failed, customer.subscription.deleted
Verification: Signature in Stripe-Signature header
Framework: Express.js
Idempotency: Store processed event IDs in Redis (24h TTL)
Error handling: Return 200 to Stripe, handle errors async
## Stripe Webhook Handler

```typescript
// webhook-handler.ts
import express, { Request, Response } from 'express';
import Stripe from 'stripe';
import { Redis } from 'ioredis';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const redis = new Redis(process.env.REDIS_URL!);
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;

// Important: Use raw body for signature verification
const router = express.Router();

router.post(
 '/webhooks/stripe',
 express.raw({ type: 'application/json' }),
 async (req: Request, res: Response) => {
 const signature = req.headers['stripe-signature'] as string;

 // 1. Verify signature
 let event: Stripe.Event;
 try {
 event = stripe.webhooks.constructEvent(
 req.body,
 signature,
 WEBHOOK_SECRET
 );
 } catch (err) {
 console.error('Webhook signature verification failed:', err);
 return res.status(400).send('Invalid signature');
 }

 // 2. Check idempotency (prevent duplicate processing)
 const idempotencyKey = `webhook:${event.id}`;
 const alreadyProcessed = await redis.get(idempotencyKey);
 
 if (alreadyProcessed) {
 console.log(`Webhook ${event.id} already processed, skipping`);
 return res.status(200).json({ received: true, duplicate: true });
 }

 // 3. Mark as processing (with short TTL in case of crash)
 await redis.setex(idempotencyKey, 60, 'processing');

 // 4. Handle event (async - don't block response)
 // Always return 200 to Stripe quickly, process async
 handleWebhookEvent(event)
.then(async () => {
 // Mark as completed with 24h TTL
 await redis.setex(idempotencyKey, 86400, 'completed');
 })
.catch(async (error) => {
 console.error(`Error processing webhook ${event.id}:`, error);
 // Remove idempotency key so it can be retried
 await redis.del(idempotencyKey);
 // Alert on-call
 await alertWebhookFailure(event, error);
 });

 // 5. Respond immediately
 return res.status(200).json({ received: true });
 }
);

// Event handler router
async function handleWebhookEvent(event: Stripe.Event): Promise<void> {
 console.log(`Processing webhook: ${event.type} (${event.id})`);

 switch (event.type) {
 case 'payment_intent.succeeded':
 await handlePaymentSucceeded(event.data.object as Stripe.PaymentIntent);
 break;

 case 'payment_intent.payment_failed':
 await handlePaymentFailed(event.data.object as Stripe.PaymentIntent);
 break;

 case 'customer.subscription.deleted':
 await handleSubscriptionCancelled(event.data.object as Stripe.Subscription);
 break;

 default:
 console.log(`Unhandled event type: ${event.type}`);
 }
}

// Individual event handlers
async function handlePaymentSucceeded(paymentIntent: Stripe.PaymentIntent): Promise<void> {
 const orderId = paymentIntent.metadata.orderId;
 
 if (!orderId) {
 console.warn('Payment succeeded but no orderId in metadata');
 return;
 }

 // Update order status
 await db.orders.update({
 where: { id: orderId },
 data: {
 status: 'paid',
 stripePaymentIntentId: paymentIntent.id,
 paidAt: new Date(),
 },
 });

 // Send confirmation email
 await emailService.sendOrderConfirmation(orderId);

 // Trigger fulfillment
 await fulfillmentService.initiate(orderId);

 console.log(`Order ${orderId} marked as paid`);
}

async function handlePaymentFailed(paymentIntent: Stripe.PaymentIntent): Promise<void> {
 const orderId = paymentIntent.metadata.orderId;
 
 if (!orderId) return;

 const lastError = paymentIntent.last_payment_error;

 await db.orders.update({
 where: { id: orderId },
 data: {
 status: 'payment_failed',
 paymentFailureReason: lastError?.message || 'Unknown error',
 },
 });

 // Notify customer
 await emailService.sendPaymentFailedEmail(orderId, lastError?.message);

 console.log(`Order ${orderId} payment failed: ${lastError?.message}`);
}

async function handleSubscriptionCancelled(subscription: Stripe.Subscription): Promise<void> {
 const userId = subscription.metadata.userId;
 
 if (!userId) {
 console.warn('Subscription cancelled but no userId in metadata');
 return;
 }

 // Update user subscription status
 await db.users.update({
 where: { id: userId },
 data: {
 subscriptionStatus: 'cancelled',
 subscriptionEndDate: new Date(subscription.current_period_end * 1000),
 },
 });

 // Send cancellation email
 await emailService.sendSubscriptionCancelledEmail(userId);

 console.log(`User ${userId} subscription cancelled`);
}

// Alerting for failed webhooks
async function alertWebhookFailure(event: Stripe.Event, error: Error): Promise<void> {
 await slack.send({
 channel: '#alerts',
 text: `⚠️ Webhook processing failed\nEvent: ${event.type}\nID: ${event.id}\nError: ${error.message}`,
 });
}

export default router;
```

### Express App Setup

```typescript
// app.ts
import express from 'express';
import webhookRouter from './webhook-handler';

const app = express();

// IMPORTANT: Webhook route must come BEFORE express.json() middleware
// because it needs the raw body for signature verification
app.use('/api', webhookRouter);

// Regular JSON parsing for other routes
app.use(express.json());

//... other routes
```

### Testing Webhooks Locally

```bash
# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward webhooks to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Trigger test events
stripe trigger payment_intent.succeeded
```

When receiving events from external services via webhooks, handling async notifications

  • Always return 200 quickly and process async, webhook providers retry on timeouts
  • Store processed event IDs to handle duplicate deliveries (webhooks are at-least-once)

3. OAuth Integration

Implement OAuth authentication flow for this service:

**Provider:** [GOOGLE / GITHUB / CUSTOM]
**Flow type:** [AUTHORIZATION CODE / IMPLICIT / CLIENT CREDENTIALS]

**Configuration:**
- Client ID: [HOW TO OBTAIN]
- Scopes needed: [PERMISSIONS REQUIRED]
- Callback URL: [YOUR REDIRECT URI]

**Requirements:**
- Framework: [EXPRESS / NEXT.JS / etc.]
- Token storage: [SESSION / DATABASE / etc.]
- Refresh handling: [HOW TO REFRESH TOKENS]

**Provide:**
1. OAuth flow implementation
2. Token management
3. Protected route middleware
4. User info fetching
Provider: GitHub
Flow: Authorization Code with PKCE
Scopes: read:user, user:email
Framework: Next.js 14 (App Router)
Token storage: HTTP-only cookies + database
Refresh: GitHub tokens don't expire, but need to handle revocation
## GitHub OAuth Implementation (Next.js 14)

### Environment Setup

```bash
#.env.local
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=random_secret_at_least_32_chars
```

### Auth Configuration

```typescript
// lib/auth.ts
import { NextAuthOptions } from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from './prisma';

export const authOptions: NextAuthOptions = {
 adapter: PrismaAdapter(prisma),
 providers: [
 GithubProvider({
 clientId: process.env.GITHUB_CLIENT_ID!,
 clientSecret: process.env.GITHUB_CLIENT_SECRET!,
 authorization: {
 params: {
 scope: 'read:user user:email',
 },
 },
 }),
 ],
 callbacks: {
 async jwt({ token, account, profile }) {
 // Initial sign in
 if (account) {
 token.accessToken = account.access_token;
 token.githubId = profile?.id;
 }
 return token;
 },
 async session({ session, token }) {
 // Add access token to session for API calls
 session.accessToken = token.accessToken as string;
 session.user.githubId = token.githubId as number;
 return session;
 },
 },
 pages: {
 signIn: '/login',
 error: '/auth/error',
 },
 session: {
 strategy: 'jwt',
 maxAge: 30 * 24 * 60 * 60, // 30 days
 },
};
```

### API Route Handler

```typescript
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
```

### Session Types

```typescript
// types/next-auth.d.ts
import { DefaultSession } from 'next-auth';

declare module 'next-auth' {
 interface Session {
 accessToken: string;
 user: {
 githubId: number;
 } & DefaultSession['user'];
 }
}

declare module 'next-auth/jwt' {
 interface JWT {
 accessToken?: string;
 githubId?: number;
 }
}
```

### Server Component Usage

```typescript
// app/dashboard/page.tsx
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
 const session = await getServerSession(authOptions);

 if (!session) {
 redirect('/login');
 }

 // Fetch user's GitHub repos using access token
 const repos = await fetchGitHubRepos(session.accessToken);

 return (
 <div>
 <h1>Welcome, {session.user.name}</h1>
 <p>Your GitHub repos:</p>
 <ul>
 {repos.map((repo) => (
 <li key={repo.id}>{repo.name}</li>
 ))}
 </ul>
 </div>
 );
}

async function fetchGitHubRepos(accessToken: string) {
 const response = await fetch('https://api.github.com/user/repos', {
 headers: {
 Authorization: `Bearer ${accessToken}`,
 Accept: 'application/vnd.github.v3+json',
 },
 next: { revalidate: 60 }, // Cache for 60 seconds
 });

 if (!response.ok) {
 throw new Error('Failed to fetch repos');
 }

 return response.json();
}
```

### Client Component Usage

```typescript
// components/LoginButton.tsx
'use client';

import { signIn, signOut, useSession } from 'next-auth/react';

export function LoginButton() {
 const { data: session, status } = useSession();

 if (status === 'loading') {
 return <button disabled>Loading...</button>;
 }

 if (session) {
 return (
 <div className="flex items-center gap-4">
 <img
 src={session.user.image || ''}
 alt={session.user.name || ''}
 className="w-8 h-8 rounded-full"
 />
 <span>{session.user.name}</span>
 <button onClick={() => signOut()}>Sign out</button>
 </div>
 );
 }

 return (
 <button onClick={() => signIn('github')}>
 Sign in with GitHub
 </button>
 );
}
```

### Protected API Route

```typescript
// app/api/user/repos/route.ts
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { NextResponse } from 'next/server';

export async function GET() {
 const session = await getServerSession(authOptions);

 if (!session?.accessToken) {
 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
 }

 try {
 const response = await fetch('https://api.github.com/user/repos', {
 headers: {
 Authorization: `Bearer ${session.accessToken}`,
 },
 });

 if (!response.ok) {
 if (response.status === 401) {
 // Token might be revoked
 return NextResponse.json(
 { error: 'GitHub token invalid' },
 { status: 401 }
 );
 }
 throw new Error('GitHub API error');
 }

 const repos = await response.json();
 return NextResponse.json(repos);
 } catch (error) {
 return NextResponse.json(
 { error: 'Failed to fetch repos' },
 { status: 500 }
 );
 }
}
```

### Token Revocation Handling

```typescript
// lib/github.ts
export async function checkTokenValidity(accessToken: string): Promise<boolean> {
 const response = await fetch('https://api.github.com/user', {
 headers: {
 Authorization: `Bearer ${accessToken}`,
 },
 });
 return response.ok;
}

// Middleware to check token on each request
export async function validateGitHubToken(
 accessToken: string
): Promise<boolean> {
 const isValid = await checkTokenValidity(accessToken);
 
 if (!isValid) {
 // Token was revoked, sign user out
 // (handle this in your session callback)
 }
 
 return isValid;
}
```

When implementing 'Sign in with X' functionality, accessing user data from external services

  • Use NextAuth.js or similar libraries, OAuth has many security pitfalls if implemented from scratch
  • Always verify tokens haven't been revoked before making API calls with them

4. Rate Limit Handler

Implement rate limit handling for this API:

**API:** [SERVICE NAME]
**Rate limits:**
- [LIMIT 1 - e.g. 100 requests/minute]
- [LIMIT 2 - e.g. 1000 requests/day]

**Headers returned:**
- [HEADER 1 - e.g. X-RateLimit-Remaining]
- [HEADER 2 - e.g. Retry-After]

**Requirements:**
- Respect limits proactively: [YES / NO]
- Queuing strategy: [FIFO / PRIORITY / etc.]
- Distributed: [SINGLE INSTANCE / MULTIPLE INSTANCES]

**Provide:**
1. Rate limiter implementation
2. Request queue/throttling
3. Retry logic
4. Monitoring/alerting
API: OpenAI API
Limits: 60 requests/minute, 90K tokens/minute
Headers: x-ratelimit-remaining-requests, x-ratelimit-reset-requests
Requirements: Proactive limiting, priority queue (paid users first), distributed across 3 servers
Language: TypeScript/Node.js
## OpenAI Rate Limit Handler

```typescript
// rate-limiter.ts
import { Redis } from 'ioredis';
import { EventEmitter } from 'events';

interface RateLimitConfig {
 requestsPerMinute: number;
 tokensPerMinute: number;
 redis: Redis;
 instanceId: string;
}

interface QueuedRequest {
 id: string;
 priority: number; // Higher = more priority
 estimatedTokens: number;
 execute: () => Promise<any>;
 resolve: (value: any) => void;
 reject: (error: Error) => void;
 enqueuedAt: number;
}

export class OpenAIRateLimiter extends EventEmitter {
 private config: RateLimitConfig;
 private queue: QueuedRequest[] = [];
 private processing = false;

 // Redis keys
 private readonly REQUEST_COUNT_KEY = 'openai:requests';
 private readonly TOKEN_COUNT_KEY = 'openai:tokens';

 constructor(config: RateLimitConfig) {
 super();
 this.config = {
 requestsPerMinute: 60,
 tokensPerMinute: 90000...config,
 };
 
 // Start queue processor
 this.processQueue();
 }

 /**
 * Queue a request to be executed respecting rate limits
 */
 async execute<T>(
 fn: () => Promise<T>,
 options: {
 priority?: number;
 estimatedTokens?: number;
 userId?: string;
 } = {}
 ): Promise<T> {
 const { priority = 5, estimatedTokens = 1000 } = options;

 return new Promise((resolve, reject) => {
 const request: QueuedRequest = {
 id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
 priority,
 estimatedTokens,
 execute: fn,
 resolve,
 reject,
 enqueuedAt: Date.now(),
 };

 // Insert by priority (higher priority = earlier in queue)
 const insertIndex = this.queue.findIndex(r => r.priority < priority);
 if (insertIndex === -1) {
 this.queue.push(request);
 } else {
 this.queue.splice(insertIndex, 0, request);
 }

 this.emit('queued', { queueLength: this.queue.length, requestId: request.id });
 });
 }

 /**
 * Check if we can make a request now
 */
 private async canMakeRequest(estimatedTokens: number): Promise<boolean> {
 const multi = this.config.redis.multi();
 
 // Get current counts (sliding window)
 const now = Date.now();
 const windowStart = now - 60000; // 1 minute ago
 
 multi.zremrangebyscore(this.REQUEST_COUNT_KEY, '-inf', windowStart);
 multi.zcard(this.REQUEST_COUNT_KEY);
 multi.zremrangebyscore(this.TOKEN_COUNT_KEY, '-inf', windowStart);
 multi.zrangebyscore(this.TOKEN_COUNT_KEY, windowStart, '+inf', 'WITHSCORES');

 const results = await multi.exec();
 const requestCount = results![1][1] as number;
 const tokenEntries = results![3][1] as string[];

 // Calculate token count from entries
 let tokenCount = 0;
 for (let i = 0; i < tokenEntries.length; i += 2) {
 tokenCount += parseInt(tokenEntries[i]);
 }

 const canMakeRequest = 
 requestCount < this.config.requestsPerMinute &&
 tokenCount + estimatedTokens <= this.config.tokensPerMinute;

 if (!canMakeRequest) {
 this.emit('rate-limited', {
 requestCount,
 tokenCount,
 requestLimit: this.config.requestsPerMinute,
 tokenLimit: this.config.tokensPerMinute,
 });
 }

 return canMakeRequest;
 }

 /**
 * Record a completed request
 */
 private async recordRequest(tokensUsed: number): Promise<void> {
 const now = Date.now();
 const multi = this.config.redis.multi();

 // Add to request count
 multi.zadd(this.REQUEST_COUNT_KEY, now, `${this.config.instanceId}:${now}`);
 multi.expire(this.REQUEST_COUNT_KEY, 120); // 2 min expiry

 // Add to token count
 multi.zadd(this.TOKEN_COUNT_KEY, now, `${tokensUsed}:${now}`);
 multi.expire(this.TOKEN_COUNT_KEY, 120);

 await multi.exec();
 }

 /**
 * Process queued requests
 */
 private async processQueue(): Promise<void> {
 while (true) {
 if (this.queue.length === 0) {
 await this.sleep(100);
 continue;
 }

 const request = this.queue[0];

 // Check if we can make request
 const canProceed = await this.canMakeRequest(request.estimatedTokens);

 if (!canProceed) {
 // Wait and retry
 await this.sleep(1000);
 continue;
 }

 // Remove from queue
 this.queue.shift();

 // Execute request
 try {
 const result = await this.executeWithRetry(request);
 request.resolve(result);
 } catch (error) {
 request.reject(error as Error);
 }
 }
 }

 /**
 * Execute request with retry on rate limit
 */
 private async executeWithRetry(request: QueuedRequest, attempt = 1): Promise<any> {
 try {
 const result = await request.execute();
 
 // Extract actual token usage from response
 const tokensUsed = result.usage?.total_tokens || request.estimatedTokens;
 await this.recordRequest(tokensUsed);

 this.emit('success', {
 requestId: request.id,
 tokensUsed,
 waitTime: Date.now() - request.enqueuedAt,
 });

 return result;
 } catch (error: any) {
 // Handle rate limit response
 if (error.status === 429) {
 const retryAfter = this.parseRetryAfter(error.headers);
 
 this.emit('rate-limit-hit', {
 requestId: request.id,
 retryAfter,
 attempt,
 });

 if (attempt < 3) {
 await this.sleep(retryAfter * 1000);
 return this.executeWithRetry(request, attempt + 1);
 }
 }

 throw error;
 }
 }

 private parseRetryAfter(headers: any): number {
 // Try Retry-After header first
 const retryAfter = headers?.['retry-after'];
 if (retryAfter) {
 return parseInt(retryAfter) || 60;
 }

 // Fall back to reset headers
 const resetRequests = headers?.['x-ratelimit-reset-requests'];
 if (resetRequests) {
 // Parse "1m30s" format
 const match = resetRequests.match(/(\d+)m?(\d+)?s?/);
 if (match) {
 return (parseInt(match[1]) || 0) * 60 + (parseInt(match[2]) || 0);
 }
 }

 return 60; // Default 1 minute
 }

 private sleep(ms: number): Promise<void> {
 return new Promise(resolve => setTimeout(resolve, ms));
 }

 /**
 * Get current queue stats
 */
 getStats(): { queueLength: number; oldestWaitTime: number } {
 return {
 queueLength: this.queue.length,
 oldestWaitTime: this.queue.length > 0 
 ? Date.now() - this.queue[0].enqueuedAt 
 : 0,
 };
 }
}
```

### Usage

```typescript
import OpenAI from 'openai';
import { OpenAIRateLimiter } from './rate-limiter';
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const rateLimiter = new OpenAIRateLimiter({
 requestsPerMinute: 60,
 tokensPerMinute: 90000,
 redis,
 instanceId: process.env.INSTANCE_ID || 'default',
});

// Monitor rate limiter
rateLimiter.on('rate-limited', (data) => {
 console.log('Approaching rate limit:', data);
});

rateLimiter.on('rate-limit-hit', (data) => {
 console.warn('Rate limit hit, retrying:', data);
});

// Use in application
export async function generateCompletion(
 prompt: string, 
 userId: string,
 isPremium: boolean
) {
 return rateLimiter.execute(
 () => openai.chat.completions.create({
 model: 'gpt-4',
 messages: [{ role: 'user', content: prompt }],
 }),
 {
 priority: isPremium ? 10 : 5, // Premium users get priority
 estimatedTokens: prompt.length / 4 + 500, // Rough estimate
 userId,
 }
 );
}
```

When integrating with APIs that have rate limits, especially when multiple instances share limits

  • Use Redis for distributed rate limiting, in-memory counters don't work across instances
  • Estimate tokens conservatively, then track actual usage to refine estimates

Common Mistakes to Avoid

Not handling rate limits, your integration breaks under load or gets blocked

Ignoring webhook signature verification, security vulnerability for spoofed events

Storing API keys in code instead of environment variables, security risk

Frequently Asked Questions

Integrating third-party APIs connects your application to external services, payments, email, analytics, and more. These prompts help you build solid integrations that handle authentication, rate limits, errors, and data transformation. The goal is integrations that work reliably in production, not just in happy-path testing.

Related Templates

Have your own prompt to optimize?