Skip to Content
IntegrationsNode.js / TypeScript

Node.js / TypeScript

Use Convoy’s REST API from Node.js and TypeScript applications. This guide provides a typed client class, webhook receiver patterns, and integration examples for Express, Next.js, and serverless functions.

Testing tip — Build and verify this integration end-to-end with convoy-mock first. It’s a free, synthetic model that returns a callback in ~60 seconds and is never billed. Once your wiring works, swap "model": "convoy-mock" for a production model like claude-3-haiku.

Quick Start

Submit a prompt in under 10 lines:

const response = await fetch('https://api.cnvy.ai/cargo/load', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.CONVOY_API_KEY!, }, body: JSON.stringify({ params: { model: 'claude-3-haiku', max_tokens: 1024, messages: [{ role: 'user', content: 'Explain quantum computing in 3 sentences.' }], }, callback_url: 'https://your-server.com/webhook/convoy', }), }); const { cargo_id } = await response.json(); console.log(`Submitted! Cargo ID: ${cargo_id}`);

Convoy processes requests in cost-efficient batches. Results arrive at your callback_url within minutes to hours, depending on queue volume and batch size (up to 24-hour SLA). For the callback receiver pattern, see the Webhooks guide.


Installation

No SDK package needed — just use the built-in fetch API (Node.js 18+) or install a lightweight HTTP client:

# Node.js 18+ has built-in fetch — no dependencies needed! # Or if you prefer axios: npm install axios # For TypeScript projects: npm install -D typescript @types/node

Typed Convoy Client

Here’s a fully-typed TypeScript client for the Convoy API:

// convoy-client.ts export interface ConvoyMessage { role: 'user' | 'assistant' | 'system'; content: string; } export interface ConvoyLoadParams { model: string; max_tokens: number; messages: ConvoyMessage[]; system?: string; temperature?: number; top_p?: number; stop_sequences?: string[]; } export interface ConvoyLoadRequest { params: ConvoyLoadParams; callback_url?: string; } export interface ConvoyLoadResponse { cargo_id: string; status: string; message: string; } export interface ConvoyUsage { input_tokens: number; output_tokens: number; } export interface ConvoyContent { type: 'text'; text: string; } export interface ConvoyResponse { id: string; type: string; role: string; content: ConvoyContent[]; model: string; stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence'; usage: ConvoyUsage; } export interface ConvoyTrackingResult { cargo_id: string; status: 'PENDING' | 'BATCHED' | 'PROCESSING' | 'COMPLETED' | 'FAILED'; response?: ConvoyResponse; error?: string; } export interface ConvoyCallbackPayload { cargo_id: string; success: boolean; response: ConvoyResponse | null; error: string | null; } export interface ConvoyModel { id: string; description?: string; } export interface ConvoyClientOptions { apiKey: string; baseUrl?: string; timeout?: number; } export class ConvoyClient { private apiKey: string; private baseUrl: string; private timeout: number; constructor(options: ConvoyClientOptions) { this.apiKey = options.apiKey; this.baseUrl = (options.baseUrl || 'https://api.cnvy.ai').replace(/\/$/, ''); this.timeout = options.timeout || 30_000; } private async request<T>(path: string, options: RequestInit = {}): Promise<T> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(`${this.baseUrl}${path}`, { ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey, ...options.headers, }, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText })); throw new ConvoyError(response.status, error.detail || 'Request failed'); } return response.json() as Promise<T>; } finally { clearTimeout(timeoutId); } } /** * Submit a prompt for batch processing. */ async load(request: ConvoyLoadRequest): Promise<ConvoyLoadResponse> { return this.request<ConvoyLoadResponse>('/cargo/load', { method: 'POST', body: JSON.stringify(request), }); } /** * Check the status of a submitted cargo request. */ async track(cargoId: string): Promise<ConvoyTrackingResult> { return this.request<ConvoyTrackingResult>(`/cargo/${cargoId}/tracking`); } /** * Poll until the cargo is complete or failed. */ async waitForResult( cargoId: string, options: { pollInterval?: number; timeout?: number } = {} ): Promise<ConvoyTrackingResult> { const { pollInterval = 10_000, timeout = 1_800_000 } = options; const start = Date.now(); while (Date.now() - start < timeout) { const result = await this.track(cargoId); if (result.status === 'COMPLETED' || result.status === 'FAILED') { return result; } await new Promise((resolve) => setTimeout(resolve, pollInterval)); } throw new ConvoyError(408, `Cargo ${cargoId} did not complete within ${timeout}ms`); } /** * List available models. */ async models(): Promise<ConvoyModel[]> { return this.request<ConvoyModel[]>('/models'); } } export class ConvoyError extends Error { constructor( public statusCode: number, message: string ) { super(message); this.name = 'ConvoyError'; } } // Helper to extract text from a response export function extractText(response: ConvoyResponse): string { return response.content .filter((c) => c.type === 'text') .map((c) => c.text) .join('\n'); }

Usage Examples

Basic: Submit and Poll

import { ConvoyClient, extractText } from './convoy-client'; const client = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY!, }); // Submit a prompt const { cargo_id } = await client.load({ params: { model: 'claude-3-haiku', max_tokens: 1024, messages: [{ role: 'user', content: 'Write a haiku about TypeScript.' }], }, }); console.log(`Submitted: ${cargo_id}`); // Wait for the result const result = await client.waitForResult(cargo_id); if (result.status === 'COMPLETED' && result.response) { console.log(extractText(result.response)); } else { console.error(`Failed: ${result.error}`); }

With Callback URL

const { cargo_id } = await client.load({ params: { model: 'claude-3-sonnet', max_tokens: 2048, messages: [{ role: 'user', content: 'Write a product launch email.' }], system: 'You are a marketing copywriter. Be engaging and concise.', temperature: 0.7, }, callback_url: 'https://your-server.com/webhook/convoy', }); console.log(`Submitted: ${cargo_id} — result will arrive at your webhook`);

Batch Submit

import { ConvoyClient, extractText } from './convoy-client'; const client = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY! }); const prompts = [ 'Write a tweet about our new AI feature', 'Generate a subject line for our newsletter', 'Create a one-paragraph product description', 'Write a customer testimonial request email', 'Draft a changelog entry for version 2.0', ]; // Submit all prompts const cargoIds = await Promise.all( prompts.map((prompt) => client.load({ params: { model: 'claude-3-haiku', max_tokens: 512, messages: [{ role: 'user', content: prompt }], system: 'You are a marketing copywriter. Be concise.', }, }) ) ); console.log(`Submitted ${cargoIds.length} prompts`); // Wait for all results const results = await Promise.all( cargoIds.map(({ cargo_id }) => client.waitForResult(cargo_id)) ); for (const result of results) { if (result.status === 'COMPLETED' && result.response) { console.log(`✅ ${extractText(result.response).slice(0, 80)}...`); } else { console.log(`❌ ${result.error}`); } }

Convoy allows 60 requests per minute and 500 per hour per project. When submitting many prompts concurrently, add rate limiting or use p-limit to control concurrency.


Webhook Receiver

Express

import express from 'express'; import type { ConvoyCallbackPayload } from './convoy-client'; const app = express(); app.use(express.json()); app.post('/webhook/convoy', (req, res) => { const payload = req.body as ConvoyCallbackPayload; const { cargo_id, success, response, error } = payload; if (success && response) { const text = response.content[0].text; console.log(`[${cargo_id}] Result: ${text.slice(0, 100)}...`); // Process the result — save to DB, notify user, etc. } else { console.error(`[${cargo_id}] Failed: ${error}`); } // Always respond 200 to acknowledge res.json({ received: true }); }); app.listen(3000, () => console.log('Webhook receiver on :3000'));

Next.js App Router

// app/api/webhook/convoy/route.ts import { NextRequest, NextResponse } from 'next/server'; import type { ConvoyCallbackPayload } from '@/lib/convoy-client'; export async function POST(request: NextRequest) { const payload: ConvoyCallbackPayload = await request.json(); const { cargo_id, success, response, error } = payload; if (success && response) { const text = response.content[0].text; // Save to database await db.results.create({ data: { cargoId: cargo_id, text, model: response.model, inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, }, }); } else { console.error(`[${cargo_id}] Error: ${error}`); // Handle failure — retry, alert, etc. } return NextResponse.json({ received: true }); }

Next.js Pages Router

// pages/api/webhook/convoy.ts import type { NextApiRequest, NextApiResponse } from 'next'; import type { ConvoyCallbackPayload } from '@/lib/convoy-client'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const payload = req.body as ConvoyCallbackPayload; if (payload.success && payload.response) { const text = payload.response.content[0].text; // Process result... } res.status(200).json({ received: true }); }

AWS Lambda (Serverless)

// handler.ts import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import type { ConvoyCallbackPayload } from './convoy-client'; export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> { const payload: ConvoyCallbackPayload = JSON.parse(event.body || '{}'); const { cargo_id, success, response, error } = payload; if (success && response) { const text = response.content[0].text; // Save to DynamoDB, S3, etc. console.log(`[${cargo_id}] Processed ${response.usage.output_tokens} tokens`); } else { console.error(`[${cargo_id}] Failed: ${error}`); } return { statusCode: 200, body: JSON.stringify({ received: true }), }; }

Error Handling

import { ConvoyClient, ConvoyError } from './convoy-client'; const client = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY! }); try { const { cargo_id } = await client.load({ params: { model: 'claude-3-haiku', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello!' }], }, }); } catch (err) { if (err instanceof ConvoyError) { switch (err.statusCode) { case 401: console.error('Invalid API key'); break; case 422: console.error(`Validation error: ${err.message}`); break; case 429: console.error('Rate limited — retry after delay'); break; default: console.error(`API error ${err.statusCode}: ${err.message}`); } } else { console.error('Network error:', err); } }

Retry with Exponential Backoff

async function loadWithRetry( client: ConvoyClient, request: ConvoyLoadRequest, maxRetries = 3 ): Promise<ConvoyLoadResponse> { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await client.load(request); } catch (err) { if (err instanceof ConvoyError) { const isRetryable = err.statusCode === 429 || err.statusCode >= 500; if (isRetryable && attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s console.log(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`); await new Promise((r) => setTimeout(r, delay)); continue; } } throw err; } } throw new Error('Max retries exceeded'); }

Integration Patterns

Pattern 1: Express API with Background Processing

import express from 'express'; import { ConvoyClient, ConvoyCallbackPayload, extractText } from './convoy-client'; const app = express(); app.use(express.json()); const client = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY! }); // In-memory store (use Redis/DB in production) const jobs = new Map<string, { userId: string; prompt: string; result?: string }>(); // User submits a prompt app.post('/api/generate', async (req, res) => { const { prompt, model = 'claude-3-haiku' } = req.body; const userId = req.headers['x-user-id'] as string; const { cargo_id } = await client.load({ params: { model, max_tokens: 2048, messages: [{ role: 'user', content: prompt }], }, callback_url: `${process.env.PUBLIC_URL}/webhook/convoy`, }); jobs.set(cargo_id, { userId, prompt }); res.json({ cargo_id, status: 'submitted' }); }); // Check job status app.get('/api/generate/:cargoId', (req, res) => { const job = jobs.get(req.params.cargoId); if (!job) return res.status(404).json({ error: 'Not found' }); res.json({ cargo_id: req.params.cargoId, status: job.result ? 'completed' : 'processing', result: job.result, }); }); // Receive Convoy callback app.post('/webhook/convoy', (req, res) => { const payload = req.body as ConvoyCallbackPayload; const job = jobs.get(payload.cargo_id); if (job && payload.success && payload.response) { job.result = extractText(payload.response); // Notify user via WebSocket, email, etc. } res.json({ received: true }); }); app.listen(3000);

Pattern 2: Queue-Based Processing with Bull

import Queue from 'bull'; import { ConvoyClient } from './convoy-client'; const client = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY! }); // Create a queue for prompt submissions const promptQueue = new Queue('convoy-prompts', process.env.REDIS_URL!); // Process jobs with rate limiting promptQueue.process(5, async (job) => { const { prompt, model, callbackUrl, metadata } = job.data; const { cargo_id } = await client.load({ params: { model: model || 'claude-3-haiku', max_tokens: 2048, messages: [{ role: 'user', content: prompt }], }, callback_url: callbackUrl, }); return { cargo_id, metadata }; }); // Add jobs to the queue export async function submitPrompt(prompt: string, metadata: Record<string, unknown>) { return promptQueue.add( { prompt, model: 'claude-3-sonnet', callbackUrl: process.env.CALLBACK_URL, metadata }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } } ); }

Pattern 3: CLI Tool

#!/usr/bin/env npx tsx // convoy-cli.ts import { ConvoyClient, extractText } from './convoy-client'; const [, , ...args] = process.argv; const prompt = args.join(' '); if (!prompt) { console.error('Usage: npx tsx convoy-cli.ts <prompt>'); process.exit(1); } const client = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY!, baseUrl: process.env.CONVOY_BASE_URL, }); console.error(`Submitting to Convoy...`); const { cargo_id } = await client.load({ params: { model: process.env.CONVOY_MODEL || 'claude-3-haiku', max_tokens: 2048, messages: [{ role: 'user', content: prompt }], }, }); console.error(`Cargo ID: ${cargo_id}`); console.error(`Waiting for result...`); const result = await client.waitForResult(cargo_id, { pollInterval: 5000 }); if (result.status === 'COMPLETED' && result.response) { console.log(extractText(result.response)); } else { console.error(`Error: ${result.error}`); process.exit(1); }

Usage:

export CONVOY_API_KEY=convoy_sk_your_key_here npx tsx convoy-cli.ts "Write a haiku about containers" npx tsx convoy-cli.ts "Generate a JSON schema for a user profile" | jq .

Environment Configuration

// config.ts import { ConvoyClient } from './convoy-client'; if (!process.env.CONVOY_API_KEY) { throw new Error('CONVOY_API_KEY environment variable is required'); } export const convoy = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY, baseUrl: process.env.CONVOY_BASE_URL || 'https://api.cnvy.ai', timeout: Number(process.env.CONVOY_TIMEOUT) || 30_000, });

.env file:

CONVOY_API_KEY=convoy_sk_your_key_here CONVOY_BASE_URL=https://api.cnvy.ai CONVOY_TIMEOUT=30000

Never commit API keys to version control. Use .env files locally and environment variables or secrets managers in production.


Rate Limiting with p-limit

Control concurrency when submitting many prompts:

import pLimit from 'p-limit'; import { ConvoyClient } from './convoy-client'; const client = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY! }); const limit = pLimit(10); // Max 10 concurrent requests const prompts = Array.from({ length: 100 }, (_, i) => `Write tip #${i + 1} for Node.js developers`); const results = await Promise.all( prompts.map((prompt) => limit(() => client.load({ params: { model: 'claude-3-haiku', max_tokens: 256, messages: [{ role: 'user', content: prompt }], }, callback_url: process.env.CALLBACK_URL, }) ) ) ); console.log(`Submitted ${results.length} prompts`);

Listing Available Models

const client = new ConvoyClient({ apiKey: process.env.CONVOY_API_KEY! }); const models = await client.models(); for (const model of models) { console.log(` ${model.id} — ${model.description || 'No description'}`); }

Troubleshooting

TypeError: fetch is not a function

  • Ensure you’re using Node.js 18+ (which includes built-in fetch)
  • For older versions, install node-fetch: npm install node-fetch

ConvoyError: 401 Unauthorized

  • Verify your API key starts with convoy_sk_
  • Check the key hasn’t been revoked in the dashboard
  • Ensure the header is X-API-Key (case-sensitive)

ConvoyError: 422 Unprocessable Entity

  • Check the request body format — messages must be an array of { role, content }
  • Verify the model ID is valid (use client.models())
  • Ensure max_tokens is a positive integer

ConvoyError: 429 Too Many Requests

  • You’ve hit the rate limit (60/min or 500/hour)
  • Implement retry with backoff (see above)
  • Use p-limit to control concurrency

Polling timeout

  • Batch processing can take minutes to hours depending on queue volume
  • Increase the timeout option in waitForResult()
  • For production, use callbacks instead of polling

AbortError: The operation was aborted

  • The request exceeded the timeout (default 30s)
  • Increase the timeout option in the client constructor
  • Check network connectivity to the Convoy API
Last updated on