> ## Documentation Index
> Fetch the complete documentation index at: https://docs.output.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# @outputai/http

> Make traceable HTTP requests from your steps

The `@outputai/http` package gives you an HTTP client that automatically shows up in your [traces](/operations/tracing). Every request — URL, method, status code, timing — is recorded as a child node in the trace tree, so you can see exactly what API calls your steps made and how long they took.

Under the hood, it wraps [ky](https://github.com/sindresorhus/ky), a lightweight HTTP client built on `fetch`.

## Creating a Client

The typical pattern is to create a client per external service in your [clients directory](/clients), then import it in your steps:

```typescript clients/jina.ts theme={null}
import { httpClient } from '@outputai/http';

export const jina = httpClient({
  prefixUrl: 'https://r.jina.ai',
  headers: {
    'Authorization': `Bearer ${process.env.JINA_API_KEY}`,
    'Accept': 'application/json'
  },
  timeout: 30000
});
```

You can extend clients to create specialized instances:

```typescript theme={null}
const authenticatedClient = jina.extend({
  headers: { 'X-Custom-Header': 'value' }
});
```

## HTTP Methods

```typescript theme={null}
// GET
const response = await client.get('https://acme.com');
const data = await response.json();

// POST
const response = await client.post('companies', {
  json: { url: 'https://acme.com' }
});
const result = await response.json();

// PUT
await client.put('companies/123', {
  json: { industry: 'SaaS' }
});

// DELETE
await client.delete('companies/123');
```

## Response Bodies

`@outputai/http` follows normal `fetch` semantics: callers own the returned response body. Always consume the body with
`.json()`, `.text()`, `.arrayBuffer()`, or another body reader when you need the payload.

If you only need response metadata from a non-`HEAD` request, such as `response.url`, `response.status`, or headers, cancel
the unused body so the underlying connection and native buffers can be released:

```typescript theme={null}
const response = await client.get('https://example.com', {
  redirect: 'follow'
});

try {
  return response.url;
} finally {
  await response.body?.cancel();
}
```

## Using in Steps

Wrap HTTP calls in steps for automatic retry and tracing:

```typescript steps.ts theme={null}
import { step } from '@outputai/core';
import { jina } from '../../clients/jina.js';
import { ScrapePageInput, ScrapePageOutput } from './types.js';

export const scrapePage = step({
  name: 'scrapePage',
  description: 'Scrape a web page using Jina Reader API',
  inputSchema: ScrapePageInput,
  outputSchema: ScrapePageOutput,
  fn: async (url) => {
    const response = await jina.get(url);
    const data = await response.json();

    return {
      title: data.data?.title ?? '',
      content: data.data?.content ?? '',
      url: data.data?.url ?? url
    };
  }
});

// types.ts
// import { z } from '@outputai/core';
//
// export const ScrapePageInput = z.string();
//
// export const ScrapePageOutput = z.object({
//   title: z.string(),
//   content: z.string(),
//   url: z.string()
// });
```

## Tracing

All requests made with `@outputai/http` are automatically traced — no configuration needed. In your trace files, HTTP calls appear as children of the step that made them:

```json theme={null}
{
  "kind": "http",
  "name": "request",
  "input": { "method": "GET", "url": "https://r.jina.ai/https://acme.com" },
  "output": { "status": 200, "statusText": "OK" }
}
```

See [Tracing](/operations/tracing) for details on the trace format.

## Request Events

Every HTTP call made through `@outputai/http`'s `fetch` (and the higher-level `httpClient`) emits an **`http:request`** hook event — independent of whether you attach a cost. Subscribe to it with [`on`](/packages/core#hooks) for logging, alerting, or per-vendor metrics:

```typescript theme={null}
import { on } from '@outputai/core/hooks';
import type { HttpRequestEvent } from '@outputai/http';

on<HttpRequestEvent>( 'http:request', async ( { eventId, workflowDetails, payload } ) => {
  if (!workflowDetails || !payload) {
    return;
  }

  console.log( `[${payload.outcome}] ${payload.method} ${payload.url} ${payload.status ?? '-'} ${payload.durationMs}ms`, {
    eventId,
    workflowId: workflowDetails.workflowId
  } );
} );
```

The handler receives an event envelope. Request-specific fields are available under `payload`:

| Field                 | Description                                                                                                                                                                                        |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventId`             | UUID v4 stamped per emit. Stable per-emit idempotency key — `http:request` and `cost:http:request` for the same fetch get distinct `eventId`s, so dedup keyed on `eventId` won't collapse the two. |
| `eventDate`           | Millisecond epoch timestamp for when the event was emitted.                                                                                                                                        |
| `activityInfo?`       | Temporal [`activity.Info`](https://typescript.temporal.io/api/interfaces/activity.Info) when the request is made in an activity context.                                                           |
| `workflowDetails?`    | Output's serializable subset of Temporal [`workflow.WorkflowInfo`](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo), when available.                                           |
| `outputActivityKind?` | Output activity kind when available. Possible values are `step`, `evaluator`, and `internal_step`.                                                                                                 |
| `payload.requestId`   | UUID generated per request inside `@outputai/http`. Shared between the matching `http:request` and `cost:http:request` events for cross-event correlation.                                         |
| `payload.method`      | HTTP method (uppercase).                                                                                                                                                                           |
| `payload.url`         | Absolute request URL.                                                                                                                                                                              |
| `payload.status`      | HTTP status code; `undefined` on network failure.                                                                                                                                                  |
| `payload.durationMs`  | Elapsed time from request issuance to response (or failure), in milliseconds.                                                                                                                      |
| `payload.outcome`     | One of `'success'` (2xx-3xx), `'error'` (status >= 400), `'failure'` (DNS / timeout / abort).                                                                                                      |

The event fires for **every** call — success, non-2xx responses, and network failures alike. The existing [`cost:http:request`](/costs/cost-events#http-request-cost) event is unchanged and continues to fire only when you call `addRequestCost`.

## Attaching Request Cost

When you know the dollar cost of an HTTP request (for example from provider billing headers), you can attach it to the HTTP trace event with `addRequestCost`.

```typescript theme={null}
import { httpClient, addRequestCost } from '@outputai/http';

const client = httpClient( { prefixUrl: 'https://api.vendor.com' } );

const response = await client.post( 'search', {
  json: { query: 'acme' }
} );

const inputCost = Number( response.headers.get( 'x-cost-input-usd' ) ?? 0 );
const outputCost = Number( response.headers.get( 'x-cost-output-usd' ) ?? 0 );

addRequestCost( response, inputCost + outputCost );

response.body?.cancel();
```

`addRequestCost` accepts the total request cost as a number.

`addRequestCost` only works with responses created by `@outputai/http` (or its exported `fetch`). If the response did not come from this package, the function safely no-ops and logs a warning.

It also emits a **`cost:http:request`** hook event (same hooks system as [`cost:llm:request`](/costs/cost-events)). For the payload and examples, see [Cost Events — HTTP](/costs/cost-events#http-request-cost).

## Error Handling

The client throws `HTTPError` for non-2xx responses and `TimeoutError` for timeouts:

```typescript theme={null}
import { HTTPError, TimeoutError } from '@outputai/http';

try {
  const response = await jina.get(url);
  return response.json();
} catch (error) {
  if (error instanceof HTTPError) {
    if (error.response.status === 404) {
      return null; // Page not found
    }
    // 429, 500, etc. — let the step retry
    throw error;
  }
  if (error instanceof TimeoutError) {
    throw error; // Step will retry
  }
  throw error;
}
```

Since steps retry automatically, you generally just need to handle cases where retrying won't help (like a 404). For everything else, let the error propagate and the step's retry policy will handle it.

## Environment Variables

| Variable                    | Description                                                                                                                              |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `OUTPUT_TRACE_HTTP_VERBOSE` | Set to `true` or `1` to include request/response headers and bodies in trace files (otherwise only method, URL, and status are recorded) |

## API Reference

For complete TypeScript API documentation, see the [HTTP Module API Reference](https://output-ai-reference-code-docs.onrender.com/modules/http_src.html).
