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_.
Authorization: Bearer sk_your_api_key_here
There are two ways to get a key:
- From the CLI.
promptconduit login(or thepromptconduit initwizard) runs a browser device flow and saves a key to~/.config/promptconduit/config.json. Add--no-browserto 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)
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
schema | number | Yes | Envelope generation. Must be 2 or higher; older envelopes are rejected with 400. |
event_id | string | Yes | Unique id for the event. The CLI uses a UUIDv7 (time-ordered). Used for deduplication; see Idempotency and retries. |
tool | string | Yes | Which AI tool produced the event. See Tool values. |
hook_event | string | Yes | The hook that fired, as the tool names it (e.g. UserPromptSubmit, PostToolUse, beforeSubmitPrompt). Must be a string; may be empty. |
captured_at | string | Yes | ISO 8601 / RFC 3339 timestamp of capture, in UTC. |
cli_version | string | Yes | Version of the producer that built the envelope. Non-empty. |
raw_event | any JSON | Yes | The tool's native hook payload, untouched. |
session_id | string | No | Session id lifted from the raw payload (for Cursor, conversation_id). Omit when the tool doesn't report one. |
prompt_id | string | No | Prompt id lifted from the raw payload (for Cursor, generation_id). |
enrichments | object | No | Slug-keyed map of locally computed context. Must be an object, not an array. |
attachments | array | No | Attachment 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).
tool | Source | Server-side normalization |
|---|---|---|
claude-code | Claude Code | Yes |
cursor | Cursor | Yes |
gemini-cli | Gemini CLI | Yes |
grok | Grok Build | Yes |
codex | OpenAI Codex CLI | Stored raw |
copilot | GitHub Copilot CLI | Stored 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/1.1 201 Created
Content-Type: application/json
X-Request-ID: …
{
"event_id": "019f2850-1234-7000-8000-abcdefabcdef",
"status": "processed"
}status | Meaning |
|---|---|
processed | Stored and normalized by the tool's adapter. |
stored_raw_only | Stored, but not normalized (no adapter for this tool, or this hook isn't one the adapter tracks). A message field explains. |
skipped | An 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
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:
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
envelopefield 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'sattachmentsarray. A file with no matching metadata fails the request. -
Every metadata entry needs
attachment_id,filename,content_type, a positivesize_bytes, andtypeset toimage,document, orfile. - 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/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. ARetry-Afterof up to 10 s is honored. Timeouts and504are not retried. -
If the send still fails with no response,
401,403,408,429, or any5xx, the envelope is queued in a local outbox and replayed after a later send succeeds. Runpromptconduit events flushto replay it now. Other4xxresponses 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.
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/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
| Status | type suffix | When | Retry? |
|---|---|---|---|
201 | — | Stored, or skipped as a duplicate. | — |
400 | validation-error | Body 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. |
401 | authentication-error | Missing, revoked, or expired API key. | After fixing the key. |
500 | internal-error | Unexpected server failure. | Yes, with backoff. |
503 | service-unavailable | Transient 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/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/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.