docs / api-reference

API Reference

The PromptConduit REST API lives at https://api.promptconduit.dev and has one primary ingest endpoint: POST /v1/events/raw, which accepts schema-2 event envelopes from the CLI. All endpoints require an API key, and errors follow RFC 7807.

Most people never call the API by hand: the CLI builds envelopes from your AI tool's hooks and sends them for you. This page documents the wire contract so you can debug what the CLI sends, or send events from a tool or exporter the CLI doesn't cover.

Authentication

Every request needs an API key, sent as a bearer token in the Authorization header. Keys start with sk_.

http
Authorization: Bearer sk_your_api_key_here

There are two ways to get a key:

  • From the CLI. promptconduit login (or the promptconduit init wizard) runs a browser device flow and saves a key to ~/.config/promptconduit/config.json. Add --no-browser to print the verification URL instead of opening it.
  • From the web app. Your profile page at app.promptconduit.dev/profile lists your keys and lets you create and revoke them. The full key is shown once, at creation; afterwards only its prefix is displayed.

To give an existing key to the CLI, run promptconduit config set --api-key=sk_…, then promptconduit test to check connectivity. See Configuration for environments and the config file. A missing, revoked, or expired key returns 401. Treat keys like passwords: anyone holding one can write events to your account.

POST /v1/events/raw

The primary ingest endpoint. Used by the CLI to forward raw events from AI tools. The server applies tool-specific adapters to normalize events into the canonical schema.

The CLI does no transformation of the tool's payload. It wraps the hook JSON exactly as the tool emitted it in raw_event, adds metadata and locally computed context, and posts it. The same envelope is also the line written to ~/.promptconduit/events.jsonl on your machine, so promptconduit events lets you inspect captured envelopes before anything is sent.

Request (JSON — envelope schema v2)

json
{
  "schema": 2,
  "event_id": "019f2850-1234-7000-8000-abcdefabcdef",
  "session_id": "9a402796-…",
  "prompt_id": "e28d55f7-…",
  "tool": "claude-code",
  "hook_event": "UserPromptSubmit",
  "captured_at": "2026-07-03T18:00:00Z",
  "cli_version": "0.9.0",
  "raw_event": { /* raw hook JSON from the AI tool, untouched */ },
  "enrichments": {
    "env":   { "host": "laptop", "os": "darwin", "arch": "arm64", "cwd": "/Users/you/my-project" },
    "trace": { "trace_id": "4bf9…", "span_id": "00f0…" },
    "vcs":   {
      "type": "github",
      "repo": "you/my-project",
      "repo_url": "https://github.com/you/my-project",
      "branch": "main",
      "commit": { "hash": "abc123", "message": "feat: x", "author": "You" },
      "dirty": false,
      "remote_url": "git@github.com:you/my-project.git"
    },
    "prompt": { "count": 3, "chars": 120, "words": 22 }
  }
}

Envelope fields

FieldTypeRequiredDescription
schemanumberYesEnvelope generation. Must be 2 or higher; older envelopes are rejected with 400.
event_idstringYesUnique id for the event. The CLI uses a UUIDv7 (time-ordered). Used for deduplication; see Idempotency and retries.
toolstringYesWhich AI tool produced the event. See Tool values.
hook_eventstringYesThe hook that fired, as the tool names it (e.g. UserPromptSubmit, PostToolUse, beforeSubmitPrompt). Must be a string; may be empty.
captured_atstringYesISO 8601 / RFC 3339 timestamp of capture, in UTC.
cli_versionstringYesVersion of the producer that built the envelope. Non-empty.
raw_eventany JSONYesThe tool's native hook payload, untouched.
session_idstringNoSession id lifted from the raw payload (for Cursor, conversation_id). Omit when the tool doesn't report one.
prompt_idstringNoPrompt id lifted from the raw payload (for Cursor, generation_id).
enrichmentsobjectNoSlug-keyed map of locally computed context. Must be an object, not an array.
attachmentsarrayNoAttachment metadata for multipart uploads. See below.

enrichments is an extensible slug-keyed map of normalized context computed by the CLI (e.g. vcs, cost, prompt). Every slug is optional and unknown slugs are stored verbatim and ignored by readers, so new enrichments require no API change. Envelopes with schema < 2 are rejected.

The well-known slugs are env (host, OS, architecture, working directory), trace (trace and span ids), vcs (repository, branch, commit, dirty state, pull request when known), prompt (prompt count, characters, words; only on prompt-submit hooks), and cost (per-request tokens and USD, with totals). The CLI also adds others such as diff, tools, permission, subagent, and turn. If you build envelopes yourself, you can send none of them; the event is still accepted.

Tool values

These are the tool values the CLI sends. Each matches the slug used by promptconduit install (see Supported Tools).

toolSourceServer-side normalization
claude-codeClaude CodeYes
cursorCursorYes
gemini-cliGemini CLIYes
grokGrok BuildYes
codexOpenAI Codex CLIStored raw
copilotGitHub Copilot CLIStored raw

Any non-empty string is accepted. When no adapter exists for the tool, the envelope is still stored and the response status is stored_raw_only instead of processed.

Response

A successful ingest returns 201 Created:

http
HTTP/1.1 201 Created
Content-Type: application/json
X-Request-ID: …

{
  "event_id": "019f2850-1234-7000-8000-abcdefabcdef",
  "status": "processed"
}
statusMeaning
processedStored and normalized by the tool's adapter.
stored_raw_onlyStored, but not normalized (no adapter for this tool, or this hook isn't one the adapter tracks). A message field explains.
skippedAn event with this event_id was already ingested. Nothing was written.

The body may include additional reference fields. Rely only on event_id, status, and message.

Example with curl

bash
curl -sS https://api.promptconduit.dev/v1/events/raw \
  -H "Authorization: Bearer $PROMPTCONDUIT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schema": 2,
    "event_id": "019f2850-1234-7000-8000-abcdefabcdef",
    "session_id": "9a402796-5c1e-4c1a-9a51-0d7f3c2b8e11",
    "tool": "claude-code",
    "hook_event": "UserPromptSubmit",
    "captured_at": "2026-07-03T18:00:00Z",
    "cli_version": "my-exporter/1.0",
    "raw_event": {
      "session_id": "9a402796-5c1e-4c1a-9a51-0d7f3c2b8e11",
      "hook_event_name": "UserPromptSubmit",
      "cwd": "/Users/you/my-project",
      "permission_mode": "default",
      "prompt": "Add a retry to the upload client"
    },
    "enrichments": {
      "prompt": { "count": 1, "chars": 32, "words": 7 }
    }
  }'

Generate a fresh event_id for every distinct event. Reusing one makes the server treat the new event as a duplicate.

Multipart Upload (with attachments)

When the event contains attachments (images, documents), the CLI sends a multipart request:

http
POST /v1/events/raw
Authorization: Bearer sk_your_api_key_here
Content-Type: multipart/form-data; boundary=---boundary

-----boundary
Content-Disposition: form-data; name="envelope"

{ ...envelope JSON, including:
  "attachments": [{
    "attachment_id": "5f0c6b2e-8d1a-4e7b-9c3f-2a6d1e4b7c90",
    "filename": "screenshot.png",
    "content_type": "image/png",
    "size_bytes": 48213,
    "type": "image"
  }] }

-----boundary
Content-Disposition: form-data; name="attachment[5f0c6b2e-8d1a-4e7b-9c3f-2a6d1e4b7c90]"; filename="screenshot.png"
Content-Type: image/png

<binary data>
-----boundary--
  • The envelope field carries the envelope JSON as a string.
  • Each file goes in a field named attachment[<attachment_id>], where the id matches an entry in the envelope's attachments array. A file with no matching metadata fails the request.
  • Every metadata entry needs attachment_id, filename, content_type, a positive size_bytes, and type set to image, document, or file.
  • The limit is 50 MB per attachment. If any attachment fails, none are kept and the request returns 400.

Idempotency and retries

The server deduplicates on event_id. A resend of an event that was already stored returns 201 with status: "skipped", so retrying after a timeout or dropped connection is always safe:

http
HTTP/1.1 201 Created
Content-Type: application/json

{
  "event_id": "019f2850-1234-7000-8000-abcdefabcdef",
  "status": "skipped",
  "message": "Duplicate event_id — already ingested"
}

Deduplication needs an id of 16–64 characters from [0-9a-zA-Z_-] (a UUID qualifies). Any other value is replaced with a server-generated id, which means retries of that event are no longer deduplicated.

What the CLI does on failure:

  • Sends happen in a detached background process, so the AI tool never waits on the network.
  • Fast failures on 429, 500, 502, 503, or a refused or reset connection are retried up to twice, with jittered delays of about 0.5 s and 2 s. A Retry-After of up to 10 s is honored. Timeouts and 504 are not retried.
  • If the send still fails with no response, 401, 403, 408, 429, or any 5xx, the envelope is queued in a local outbox and replayed after a later send succeeds. Run promptconduit events flush to replay it now. Other 4xx responses are permanent and the envelope is not retried.

If you write your own client, follow the same rule: retry 429 and 5xx with backoff, honor Retry-After, and don't retry 400.

OTLP / OpenTelemetry

Export traces and logs from Claude Code, Cursor, or any OTLP-compatible agent directly to PromptConduit. OTLP data lands in the same event store as CLI envelopes, under the same account. For Claude Code, the Claude Code Telemetry guide has the full setup; its api_request events count toward team cost.

shell
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.promptconduit.dev/v1/otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer sk_your_api_key_here"

Endpoints: POST /v1/otlp/v1/traces, POST /v1/otlp/v1/logs, and POST /v1/otlp/v1/metrics. Bodies may be OTLP/HTTP protobuf (application/x-protobuf) or JSON (application/json), optionally with Content-Encoding: gzip; gRPC is not served. Any other content type returns 415, and a body that doesn't decode returns 400. Metrics are accepted and discarded so all three signals can be enabled safely. A successful export returns 200 with an empty response in the request's encoding ({} for JSON).

Error Format

Errors follow RFC 7807. They are returned with Content-Type: application/problem+json and always carry type, title, status, and detail, plus instance (the request path) and request_id. The same request id is in the X-Request-ID response header; include it when you report a problem. Branch on type or status, not on detail, which is human-readable and may change.

http
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json

{
  "type": "https://api.promptconduit.dev/errors/authentication-error",
  "title": "Authentication Error",
  "status": 401,
  "detail": "API key required",
  "instance": "/v1/events/raw",
  "request_id": "…"
}

Status codes for /v1/events/raw

Statustype suffixWhenRetry?
201—Stored, or skipped as a duplicate.—
400validation-errorBody isn't valid JSON, an envelope field is missing or has the wrong type, schema is below 2, the multipart form has no envelope field, or an attachment is invalid or over 50 MB.No. Fix the request.
401authentication-errorMissing, revoked, or expired API key.After fixing the key.
500internal-errorUnexpected server failure.Yes, with backoff.
503service-unavailableTransient storage error before the event was committed. Sends Retry-After.Yes, after Retry-After.

Every type is the prefix https://api.promptconduit.dev/errors/ plus the suffix. A validation error lists every problem it found in detail:

http
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://api.promptconduit.dev/errors/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "Invalid envelope: Unsupported envelope schema (need schema >= 2); v1 envelopes are no longer accepted — upgrade the CLI, Missing raw_event",
  "instance": "/v1/events/raw",
  "request_id": "…"
}

A transient failure tells you when to retry, in both the header and the body:

http
HTTP/1.1 503 Service Unavailable
Content-Type: application/problem+json
Retry-After: 2

{
  "type": "https://api.promptconduit.dev/errors/service-unavailable",
  "title": "Service Unavailable",
  "status": 503,
  "detail": "Temporary storage error; retry the request",
  "instance": "/v1/events/raw",
  "request_id": "…",
  "retry_after": 2
}

When to use the API directly vs the CLI

Use the CLI for Claude Code, Cursor, Gemini CLI, Codex, Copilot CLI, and Grok Build. promptconduit install <tool> wires up the hooks, and the CLI adds git, cost, and trace enrichments, uploads attachments, writes a local log, and handles retries and the outbox. You also get the free local features (cost, sessions, local skills) with no account. See the Quick Start and the CLI Reference.

Use the API directly when the CLI can't run where events happen (a hosted agent, a CI job without the binary), when you're building an integration for a tool the CLI doesn't support yet, or when your agent already emits OpenTelemetry, in which case point its OTLP exporter at /v1/otlp and skip envelopes entirely. You are then responsible for minting unique event_ids and retrying safely.

If you only want local cost tracking in your editor, you don't need the API at all: the editor extension reads the local event log.