fetch calls works, but you lose visibility into what’s happening across your workflow and becomes tedious to maintain.
Output ships httpClient from @outputai/http as the standard way to write API clients. Every request is automatically traced, so you see exactly what your workflow called in the same trace as the step that called it, what came back, and how long it took. You (or Claude Code) write thin, typed wrappers — one file per API — that your steps import.
Your First Client
Here’s a simple client that searches the web using Tavily. Create a file atsrc/clients/tavily.ts:
src/clients/tavily.ts
createClient() builds an HTTP client with a base URL and timeout. Every request through this client is automatically traced — you’ll see the URL, status code, and timing in your workflow trace. The prefixUrl means you only write relative paths like 'search' instead of the full URL.
getApiKey() reads the API key from an environment variable. It throws a FatalError if the key is missing — this tells Output “don’t retry this step, it will never succeed without the key.” More on this in Error Handling.
The exported object (tavilyClient) exposes typed methods that your steps call. Steps never see HTTP details — they call tavilyClient.search('AI frameworks') and get back an array of results.
Use it from a step like this:
steps.ts
Authentication Patterns
Different APIs authenticate differently. The client above passes the API key in the request body, but most APIs use headers. Here are the common patterns.Bearer Token
The most common pattern — pass a token in theAuthorization header. Many internal APIs and services like OpenAI, Perplexity, and HubSpot use this:
src/clients/hubspot.ts
API Key in Headers
Some APIs use a custom header name likex-api-key instead of the standard Authorization header:
src/clients/exa.ts
createClient() instead if every request uses the same key. Per-request headers are useful when you want the base client to be reusable.
Basic Auth
Older APIs sometimes use HTTP Basic authentication, which encodes a username and password as a Base64 string.Buffer.from() is Node.js’s built-in way to do this encoding:
src/clients/image_service.ts
Validating Responses with Zod
API responses can change without warning — a field gets renamed, a type changes, a new required field appears. If you pass unvalidated data through your workflow, these changes cause confusing errors far from the source. Zod schemas catch this at the boundary. Define what the API should return, and parse the response immediately:src/clients/clearbit.ts
estimatedAnnualRevenue from a string to a number, CompanySchema.parse() throws immediately with a clear message like Expected string, received number at "metrics.estimatedAnnualRevenue". You’ll know exactly what broke and where.
The z.infer<typeof CompanySchema> line generates a TypeScript type from the schema, so you get autocomplete and type checking throughout your workflow without writing the type separately.
Error Handling
When an API call fails, you need to decide: should Output retry the step, or is the error permanent?httpClient throws two error types:
HTTPError— The server responded with a non-2xx status code (like 404, 429, or 500)TimeoutError— The request took longer than the timeout you set
src/clients/search.ts
FatalError for configuration problems. Output’s retry policy handles the rest — you set the retry count and backoff in your step options or workflow options.
Adding More Methods
As you integrate deeper with an API, you’ll need more than one method. Group related operations in a single client object. Here’s what a multi-method client looks like:src/clients/web_search.ts
webSearchClient.search(...), webSearchClient.extract(...) — without knowing the HTTP details.
Async Task Polling
Some APIs don’t return results immediately. Instead, they give you a task ID and you poll until the work is done. Research APIs and batch processing services commonly work this way:src/clients/research.ts
Project Organization
Put clients insrc/clients/, one file per external API:
httpClient instances directly. When an API changes its auth or base URL, you update one file and every workflow that uses it picks up the change.
Debugging
When an API call isn’t returning what you expect, turn on verbose logging to see the full request and response bodies in your traces:httpClient — request bodies, response bodies, headers, timing. Sensitive headers like authorization and api-key are automatically redacted so you can safely share traces.
Turn this off in production. Response bodies from search APIs and content services can be large, and you don’t want that in every trace.
Complete Example: Tavily Client
Here’s the full Tavily client with search, extract, and crawl methods. This is a production-ready example you can copy and adapt — it demonstrates environment variable validation, typed interfaces, multiple methods, and transforming the API’ssnake_case responses into camelCase for your TypeScript code:
src/clients/tavily.ts
What’s Next
- @outputai/http API reference — Full
httpClientAPI,.extend(), and configuration options - Steps — Where clients get used: wrapping API calls in retryable steps
- Tracing — How request traces appear in the Temporal UI
- Error Handling —
FatalError,ValidationError, and retry policies