@venturekit/integrations API
Overview
Section titled “Overview”@venturekit/integrations provides tools for calling third-party APIs from your Lambda functions:
- HTTP Client — configurable
fetch-based client with retry, timeout, interceptors - OAuth2 — client credentials & authorization code flows with automatic token refresh
- API Key Management — retrieve and cache keys from env vars, SSM, or Secrets Manager
npm install @venturekit/integrationsHTTP Client
Section titled “HTTP Client”apiClient(options)
Section titled “apiClient(options)”Create a configured HTTP client for calling external APIs.
import { apiClient } from '@venturekit/integrations';
const stripe = apiClient({ baseUrl: 'https://api.stripe.com/v1', headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` }, retry: { maxRetries: 3 }, timeout: 10_000,});
const customer = await stripe.post<StripeCustomer>('/customers', { body: { email: 'user@example.com' },});// customer.data, customer.status, customer.headersApiClientOptions
Section titled “ApiClientOptions”| Option | Type | Description |
|---|---|---|
baseUrl | string | Base URL for all requests |
headers | Record<string, string> | Default headers |
timeout | number | Default timeout in ms (default: 30000) |
retry | RetryOptions | Retry configuration |
onRequest | RequestInterceptor[] | Interceptors called before each request |
onResponse | ResponseInterceptor[] | Interceptors called after each response |
onError | ErrorInterceptor | Called on any error before it’s thrown |
RetryOptions
Section titled “RetryOptions”| Option | Type | Default | Description |
|---|---|---|---|
maxRetries | number | 0 | Max retry attempts |
initialDelayMs | number | 1000 | Initial backoff delay |
backoffMultiplier | number | 2 | Exponential backoff multiplier |
retryableStatuses | number[] | [408, 429, 500, 502, 503, 504] | HTTP statuses that trigger retry |
ApiRequestOptions
Section titled “ApiRequestOptions”| Option | Type | Description |
|---|---|---|
body | unknown | Request body (auto-JSON-stringified) |
headers | Record<string, string> | Per-request headers |
params | Record<string, string> | Query parameters |
timeout | number | Timeout override (ms) |
form | boolean | Send as application/x-www-form-urlencoded |
signal | AbortSignal | Cancellation signal |
ApiResponse<T>
Section titled “ApiResponse<T>”interface ApiResponse<T> { status: number; headers: Record<string, string>; data: T; retried: boolean; attempts: number;}ApiError
Section titled “ApiError”Thrown on non-2xx responses or network errors.
class ApiError extends Error { status: number; headers: Record<string, string>; data: unknown; url: string; method: string;}OAuth2
Section titled “OAuth2”oauth2(options)
Section titled “oauth2(options)”Create an OAuth2 client for automatic token management.
Client Credentials Flow
Section titled “Client Credentials Flow”Server-to-server authentication (no user interaction):
import { oauth2, apiClient } from '@venturekit/integrations';
const auth = oauth2({ flow: 'client_credentials', tokenUrl: 'https://oauth.provider.com/token', clientId: process.env.CLIENT_ID!, clientSecret: process.env.CLIENT_SECRET!, scopes: ['read', 'write'],});
// Attach to an API client — tokens refresh automaticallyconst client = apiClient({ baseUrl: 'https://api.provider.com', onRequest: [auth.interceptor()],});Authorization Code Flow
Section titled “Authorization Code Flow”User-delegated authentication (e.g. Google, Slack):
const auth = oauth2({ flow: 'authorization_code', tokenUrl: 'https://oauth2.googleapis.com/token', authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth', clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, redirectUri: 'https://myapp.com/callback', scopes: ['profile', 'email'], store: await createDynamoDBTokenStore({ tableName: 'oauth-tokens' }),});
// 1. Redirect user to authorizeconst url = auth.getAuthorizeUrl!('random-state');
// 2. Exchange code in callback handlerconst token = await auth.exchangeCode!('auth-code-from-callback');
// 3. Use with API clientconst google = apiClient({ baseUrl: 'https://www.googleapis.com', onRequest: [auth.interceptor()],});OAuth2Client Methods
Section titled “OAuth2Client Methods”| Method | Description |
|---|---|
getToken() | Get a valid access token (auto-refreshes) |
refreshToken() | Force a token refresh |
interceptor() | Returns a request interceptor that attaches the Bearer token |
exchangeCode(code) | Exchange an authorization code (auth code flow only) |
getAuthorizeUrl(state?) | Build the authorization URL (auth code flow only) |
Token Storage
Section titled “Token Storage”| Store | Description |
|---|---|
createMemoryTokenStore() | In-memory (default) — resets on cold start |
createDynamoDBTokenStore({ tableName }) | DynamoDB — persists across invocations |
API Key Management
Section titled “API Key Management”apiKey(source)
Section titled “apiKey(source)”Create a handle for retrieving and caching an API key.
import { apiKey, apiKeyHeader, apiClient } from '@venturekit/integrations';
// From environment variableconst stripeKey = apiKey({ env: 'STRIPE_SECRET_KEY' });
// From SSM Parameter Storeconst twilioKey = apiKey({ ssm: '/myapp/prod/twilio-key' });
// From Secrets Manager (with JSON field extraction)const slackToken = apiKey({ secret: 'myapp/slack-bot-token', field: 'token',});
// Use with apiClientconst stripe = apiClient({ baseUrl: 'https://api.stripe.com/v1', onRequest: [apiKeyHeader(stripeKey)],});Sources
Section titled “Sources”| Source | Fields | Description |
|---|---|---|
{ env: string } | Environment variable name | Reads from process.env |
{ ssm: string, region?, withDecryption? } | SSM path | Reads from Parameter Store |
{ secret: string, field?, region? } | Secret name/ARN | Reads from Secrets Manager |
ApiKeyHandle
Section titled “ApiKeyHandle”| Method | Description |
|---|---|
get() | Get the key (cached after first call, 5-min TTL) |
refresh() | Force a re-fetch |
clear() | Clear the cache |
apiKeyHeader(handle, headerName?, prefix?)
Section titled “apiKeyHeader(handle, headerName?, prefix?)”Convenience function that returns a request interceptor attaching the key as a header.
// Default: Authorization: Bearer <key>apiKeyHeader(myKey)
// Custom header, no prefix: X-Api-Key: <key>apiKeyHeader(myKey, 'X-Api-Key', '')Outbound Webhooks
Section titled “Outbound Webhooks”Deliver your events to your customers’ endpoints. @venturekit/data’s outbox and the runtime’s publishEvent move events inside the platform; this moves them out. Every function takes a WebhookQuerier ((sql, params?) => Promise<rows> — @venturekit/data’s query or a transaction’s tx.query). Tables vk_webhook_endpoints and vk_webhook_deliveries ship in this package’s migrations.
Endpoints
Section titled “Endpoints”| Function | Description |
|---|---|
createWebhookEndpoint(q, { url, tenantId?, eventTypes?, description?, secret? }) | { endpoint, secret } — the plaintext secret (whsec_ + 32 random bytes, base64url) is returned only here; eventTypes empty / omitted subscribes to every event. url must be https:// (http:// only for localhost) |
listWebhookEndpoints(q, { tenantId?, includeDisabled? }) / getWebhookEndpoint(q, id) | Read |
updateWebhookEndpoint(q, id, { url?, eventTypes?, description?, enabled? }) | Patch; returns the endpoint or null |
rotateWebhookEndpointSecret(q, id) | New secret, returned once; deliveries signed from now on use it |
deleteWebhookEndpoint(q, id) | Removes the endpoint and, by FK cascade, its delivery log |
Enqueue & deliver
Section titled “Enqueue & deliver”| Function | Description |
|---|---|
enqueueWebhook(q, { eventId, eventType, payload, tenantId?, maxAttempts? }) | Fan out to every enabled endpoint subscribed to eventType within the tenant scope (tenantId omitted / null = tenant-less endpoints); returns the number of new delivery rows — an existing (endpoint, eventId) pair is skipped, so retrying the producer is safe. Call it with the transaction’s tx.query so deliveries commit with the change they announce. maxAttempts default 8 |
deliverWebhooksOnce(q, { limit?, timeoutMs?, fetch?, headers?, now? }) | One cron tick: claim due deliveries (FOR UPDATE SKIP LOCKED, limit default 25), POST the signed JSON with a per-request timeout (default 10 000 ms), mark 2xx delivered, reschedule anything else with webhookBackoffSeconds, mark dead at maxAttempts; endpoints disabled meanwhile get their pending rows marked dead without a request. Returns { claimed, delivered, retried, dead }. Safe to run concurrently |
webhookBackoffSeconds(attempts) | 30 s, 1 min, 2 min, 4 min, … capped at 6 h — with 8 attempts a dead endpoint is retried for ≈ 4 h |
listWebhookDeliveries(q, { endpointId, status?, limit?, before? }) | The customer-facing delivery log, newest first (limit 1–200, default 50; before is a keyset cursor on createdAt) |
replayWebhookDelivery(q, id) | Put a dead (or delivered) row back in the queue for an immediate retry |
Signing
Section titled “Signing”Each request carries X-VK-Webhook-Id, X-VK-Event-Id, X-VK-Event-Type, X-VK-Timestamp (Unix seconds) and X-VK-Signature (WEBHOOK_HEADERS). The signed string is ${timestamp}.${rawBody}; the header value is v1=<hex HMAC-SHA256>.
| Function | Description |
|---|---|
signWebhookPayload(secret, timestamp, rawBody) | Producer side — what deliverWebhooksOnce sends |
verifyWebhookSignature({ secret, timestamp, rawBody, signature, toleranceSeconds?, now? }) | Receiver side: constant-time compare, timestamps outside toleranceSeconds (default 300) rejected to bound replay; accepts a comma-separated list of v1= values so a secret rotation can overlap. Give this to your customers (or its algorithm — it is standard HMAC) |
generateWebhookSecret() | whsec_… |
// The customer's receiverconst ok = verifyWebhookSignature({ secret: process.env.VK_WEBHOOK_SECRET!, timestamp: req.headers['x-vk-timestamp'], rawBody: req.rawBody, // the bytes as received — not a re-serialised object signature: req.headers['x-vk-signature'],});if (!ok) return res.status(400).end();if (await seen(req.headers['x-vk-event-id'])) return res.status(200).end(); // dedupe