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

# Manual capture

> Build and capture canonical events directly when wrappers don't fit your stack.

Wrappers cover the OpenAI, Anthropic, and Vercel AI SDKs. For anything else — custom inference stacks, other providers, backfills — construct canonical events yourself and capture them directly.

## Capture methods

```typescript theme={null}
carbon.captureLlmCall({ event });   // one LLM event
carbon.captureToolCall({ event });  // one tool event
carbon.captureEvents({ events });   // a mixed batch
```

All three validate events synchronously against the canonical schema and throw on invalid input, then buffer the events for delivery like any wrapper-captured event. The full field reference is in the [event schema](/api-reference/schemas/event-schema).

## LLM event example

```typescript theme={null}
import { randomUUID } from "node:crypto";

const startTimeMs = Date.now();
const result = await myInferenceCall(prompt);
const endTimeMs = Date.now();

carbon.captureLlmCall({
  event: {
    type: "llm",
    id: randomUUID(),
    startTimeMs,
    endTimeMs,
    durationMs: endTimeMs - startTimeMs,
    status: { state: "ok" },
    instrumentation: {
      vendor: "carbon",
      source: "openai",
      sourcePackage: "openai",
    },
    context: { userId: "user-481" },
    additionalProperties: {},
    properties: {
      llm: {
        model: "gpt-5.4-nano",
        input: { system: "You are concise.", prompt, tools: [] },
        output: { mode: "generate", response: result.text, toolCalls: [] },
        usage: {
          inputTokens: result.usage.inputTokens,
          inputTokenDetails: {
            uncachedTokens: result.usage.inputTokens,
            cacheReadTokens: 0,
            cacheWriteTokens: 0,
          },
          outputTokens: result.usage.outputTokens,
          outputTokenDetails: {
            reasoningTokens: 0,
            responseTokens: result.usage.outputTokens,
          },
          totalTokens: result.usage.inputTokens + result.usage.outputTokens,
        },
      },
    },
  },
});
```

<Note>
  Omit `spaceId` and `cost` — the ingest API sets the space from your API key
  and computes cost from its model catalog when it recognizes the model.
</Note>

## Tool event example

```typescript theme={null}
const startTimeMs = Date.now();
const output = await searchIndex({ query });
const endTimeMs = Date.now();

carbon.captureToolCall({
  event: {
    type: "tool",
    id: randomUUID(),
    traceId,
    startTimeMs,
    endTimeMs,
    durationMs: endTimeMs - startTimeMs,
    status: { state: "ok" },
    instrumentation: {
      vendor: "carbon",
      source: "vercel",
      sourcePackage: "ai",
    },
    context: { agentId: "search-agent" },
    additionalProperties: {},
    properties: {
      tool: {
        name: "search_index",
        input: JSON.stringify({ query }),
        output: JSON.stringify(output),
      },
    },
  },
});
```

## Recording failures

Set `status.state` to `"error"` and include what you know about the failure:

```typescript theme={null}
status: {
  state: "error",
  error: {
    code: "rate_limit_exceeded",
    httpStatus: 429,
    message: "Provider rate limit hit",
  },
},
```

Failed calls appear in the dashboard with error status, so error rates can be sliced by model, agent, and user like any other dimension.

## Sending without the SDK

If you are not on JavaScript, post the same event shapes to the ingest API directly — see the [API reference](/api-reference/overview/introduction).
