Skip to content

@venturekit/integrations API

@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
Terminal window
npm install @venturekit/integrations

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.headers
OptionTypeDescription
baseUrlstringBase URL for all requests
headersRecord<string, string>Default headers
timeoutnumberDefault timeout in ms (default: 30000)
retryRetryOptionsRetry configuration
onRequestRequestInterceptor[]Interceptors called before each request
onResponseResponseInterceptor[]Interceptors called after each response
onErrorErrorInterceptorCalled on any error before it’s thrown
OptionTypeDefaultDescription
maxRetriesnumber0Max retry attempts
initialDelayMsnumber1000Initial backoff delay
backoffMultipliernumber2Exponential backoff multiplier
retryableStatusesnumber[][408, 429, 500, 502, 503, 504]HTTP statuses that trigger retry
OptionTypeDescription
bodyunknownRequest body (auto-JSON-stringified)
headersRecord<string, string>Per-request headers
paramsRecord<string, string>Query parameters
timeoutnumberTimeout override (ms)
formbooleanSend as application/x-www-form-urlencoded
signalAbortSignalCancellation signal
interface ApiResponse<T> {
status: number;
headers: Record<string, string>;
data: T;
retried: boolean;
attempts: number;
}

Thrown on non-2xx responses or network errors.

class ApiError extends Error {
status: number;
headers: Record<string, string>;
data: unknown;
url: string;
method: string;
}

Create an OAuth2 client for automatic token management.

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 automatically
const client = apiClient({
baseUrl: 'https://api.provider.com',
onRequest: [auth.interceptor()],
});

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 authorize
const url = auth.getAuthorizeUrl!('random-state');
// 2. Exchange code in callback handler
const token = await auth.exchangeCode!('auth-code-from-callback');
// 3. Use with API client
const google = apiClient({
baseUrl: 'https://www.googleapis.com',
onRequest: [auth.interceptor()],
});
MethodDescription
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)
StoreDescription
createMemoryTokenStore()In-memory (default) — resets on cold start
createDynamoDBTokenStore({ tableName })DynamoDB — persists across invocations

Create a handle for retrieving and caching an API key.

import { apiKey, apiKeyHeader, apiClient } from '@venturekit/integrations';
// From environment variable
const stripeKey = apiKey({ env: 'STRIPE_SECRET_KEY' });
// From SSM Parameter Store
const 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 apiClient
const stripe = apiClient({
baseUrl: 'https://api.stripe.com/v1',
onRequest: [apiKeyHeader(stripeKey)],
});
SourceFieldsDescription
{ env: string }Environment variable nameReads from process.env
{ ssm: string, region?, withDecryption? }SSM pathReads from Parameter Store
{ secret: string, field?, region? }Secret name/ARNReads from Secrets Manager
MethodDescription
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', '')

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.

FunctionDescription
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
FunctionDescription
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

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>.

FunctionDescription
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 receiver
const 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