Private write key
Ingest trusted server events with /v1/events. Never expose this credential in a browser.
Developer reference v1 ingestion · tRPC queries
Send explicit workflow events, inspect project activity, and follow a run from start to outcome. Everything the current API exposes, in one place.
https://api.flowmark.devJSON over HTTPProtected operations use Authorization: Bearer <key>. Keys resolve a project and a specific permission; read, server-write, and browser keys are not interchangeable. Health checks require no key.
Ingest trusted server events with /v1/events. Never expose this credential in a browser.
Read project data through tRPC. Keep the key behind a same-origin dashboard proxy.
Write only, exact allowed origins, 120 events/minute/key, and a browser. workflow namespace.
This page never loads credentials or executes writes. Examples are inert until you run them. Replace placeholders with your own provisioned keys; no key creation or management endpoint exists here. Fixed demo credentials are rejected unless FLOWMARK_DEMO_MODE=true.
Expand an operation for its request, response schema, and error codes. Examples assume FLOWMARK_API_URL=https://api.flowmark.dev and your own server-side key environment variables. Sending an example POST persists data.
/api/healthCheck API livenessNo keyA database-independent liveness check. This confirms the HTTP service responds, not that PostgreSQL or ingestion is ready.
curl "$FLOWMARK_API_URL/api/health"{"status":"ok","service":"flowmark-api"}| HTTP | Meaning |
|---|---|
200 | The API HTTP service is alive. |
/v1/eventsIngest a server workflow eventPrivate write keyAccepts one event envelope, resolves the project from the key, and acknowledges only after persistence. Server-side only: no browser CORS handler is provided here.
Keep write keys on a trusted server. Never put them in NEXT_PUBLIC_* variables, browser bundles, screenshots, or client-side requests. Reuse the same eventId when retrying the same event.
curl "$FLOWMARK_API_URL/v1/events" \
-H "Authorization: Bearer $FLOWMARK_WRITE_KEY" \
-H "Content-Type: application/json" \
--data '{
"eventId": "11111111-1111-4111-8111-111111111111",
"event": {
"type": "started",
"workflow": "checkout",
"runId": "checkout-example-001",
"timestamp": "2026-01-01T12:00:00.000Z",
"properties": {
"source": "reference-example"
},
"correlationId": "checkout-request-123",
"parentRunId": "browser-run-id-from-application-propagation"
}
}'{"accepted":true}| HTTP | Meaning |
|---|---|
202 | Persisted, or duplicate event ID ignored. {"accepted":true} |
400 | Invalid JSON, invalid event envelope, or body over 64 KiB. |
401 | Missing, invalid, or wrong-purpose Bearer key. |
409 | Send the started event before other run events. |
503 | Configuration or event storage unavailable. |
/v1/browser-eventsIngest an untrusted browser eventPublic browser key + OriginUses a separate browser credential with an exact origin allowlist. Workflow names are prefixed with browser. unless already prefixed. Browser telemetry is not trusted server evidence.
The browser sets Origin; JavaScript cannot choose it. Public keys cannot query data or use private ingestion. CORS is a browser restriction, not proof of caller identity: a non-browser client can forge Origin. The shared key quota is consumed before payload validation. Auth/origin failures and unavailable-storage responses do not include CORS headers.
// Run only from an origin configured for YOUR public browser key.
// Explicit example: this writes an event when you execute it.
await fetch("https://api.flowmark.dev/v1/browser-events", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_PUBLIC_BROWSER_KEY",
},
body: JSON.stringify({
"eventId": "11111111-1111-4111-8111-111111111111",
"event": {
"type": "started",
"workflow": "checkout",
"runId": "checkout-example-001",
"timestamp": "2026-01-01T12:00:00.000Z",
"properties": {
"source": "reference-example"
},
"correlationId": "checkout-request-123",
"parentRunId": "browser-run-id-from-application-propagation"
}
}),
});{"accepted":true}| HTTP | Meaning |
|---|---|
202 | Persisted, or duplicate event ID ignored. {"accepted":true} |
400 | Invalid JSON, invalid event envelope, or body over 64 KiB. |
401 | Missing, invalid, or wrong-purpose Bearer key. |
403 | Missing Origin, or origin not allowed for this browser key. |
409 | Send the started event before other run events. |
429 | 120 events/minute/key exceeded; Retry-After: 60. |
503 | Configuration or event storage unavailable. |
/v1/browser-eventsNegotiate browser ingestion CORSAllowed OriginPreflight checks whether any configured browser key allows the origin and the requested method is POST. It does not authenticate a key; the subsequent POST checks that key’s own allowlist.
curl -i -X OPTIONS "$FLOWMARK_API_URL/v1/browser-events" \
-H "Origin: http://localhost:3004" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type, authorization"HTTP 204 (empty body)
Access-Control-Allow-Origin: <allowed request origin>
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: content-type, authorization, traceparent, b3
Access-Control-Max-Age: 600
Vary: Origin
Cache-Control: no-store| HTTP | Meaning |
|---|---|
204 | Allowed origin and POST requested. |
403 | Missing/disallowed origin or requested method is not POST. |
503 | Configuration or origin lookup unavailable. |
/api/trpc/{procedure}Typed query transport · GET / POST / OPTIONSDepends on procedureThe route exports GET, POST, and OPTIONS. All current procedures are queries: use GET. POST is wired to the tRPC adapter, but there are no mutations and query-over-POST is not enabled.
The dashboard CORS origin is https://app.flowmark.dev. CORS does not authorize a read; protected procedures still require a read key. Prefer a same-origin server proxy to keep dashboard read keys out of browsers. Standard tRPC batching may return 207 for mixed outcomes.
curl -i -X OPTIONS "$FLOWMARK_API_URL/api/trpc/project.snapshot" \
-H "Origin: https://app.flowmark.dev" \
-H "Access-Control-Request-Method: GET"HTTP 204 (empty body)
// Only when Origin equals FLOWMARK_DASHBOARD_ORIGIN:
Access-Control-Allow-Origin: <configured dashboard origin>
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: content-type, authorization
Vary: Origin| HTTP | Meaning |
|---|---|
204 | OPTIONS returns no body; unrecognized origins get no CORS grant. |
400 | Malformed input or invalid run UUID (BAD_REQUEST). |
404 | Unknown procedure (NOT_FOUND). |
405 | Wrong method, including POST to a query (METHOD_NOT_SUPPORTED). |
415 | Unsupported or missing POST Content-Type (UNSUPPORTED_MEDIA_TYPE). |
/api/trpc/health.checkCheck liveness through tRPCNo keyThe typed equivalent of the HTTP health check. No input, authentication, or database access.
curl "$FLOWMARK_API_URL/api/trpc/health.check"{"result":{"data":{"status":"ok","service":"flowmark-api"}}}| HTTP | Meaning |
|---|---|
200 | Service responds with a tRPC result envelope. |
/api/trpc/project.snapshotRead the project overviewPrivate read keyNo input. Returns the latest 100 runs, workflow summaries, volume/success/duration metrics, 30-day daily insight, slowest operations, and failure codes. Project scope comes from the read key, never a client-supplied project ID.
Success rate is a percentage across terminal runs and is null when no runs have finished. Keep the read key server-side: it grants access to project workflow data.
curl "$FLOWMARK_API_URL/api/trpc/project.snapshot" \
-H "Authorization: Bearer $FLOWMARK_READ_KEY"// Schema of result.data (not a literal JSON response)
{
runs: Array<{
id: UUID, externalId: string, workflow: string, startedAt: string,
status: "Completed" | "Failed" | "Running",
durationMs: number | null, step: string | null,
correlationId: string | null, parentRunId: string | null
}>,
workflows: Array<{
name: string, runs: number, successRate: number | null,
medianDurationMs: number, p95DurationMs: number
}>,
metrics: {
totalRuns: number, failedRuns: number,
successRate: number | null, medianDurationMs: number, p95DurationMs: number
},
daily: Array<{
date: string, count: number, completed: number, failed: number,
successRate: number | null, medianDurationMs: number | null,
p95DurationMs: number | null
}>,
operations: Array<{
name: string, workflow: string, count: number, failed: number,
medianDurationMs: number, p95DurationMs: number, totalDurationMs: number
}>,
failureCodes: Array<{ code: string, workflow: string, count: number }>
}| HTTP | Meaning |
|---|---|
200 | Query result in a tRPC result.data envelope. |
401 | Invalid dashboard read key (UNAUTHORIZED). |
500 | Configuration or database read failed (INTERNAL_SERVER_ERROR). |
/api/trpc/workflow.listList workflow summariesPrivate read keyNo input. Returns the workflows portion of the authorized project snapshot, ordered by workflow name.
curl "$FLOWMARK_API_URL/api/trpc/workflow.list" \
-H "Authorization: Bearer $FLOWMARK_READ_KEY"// Schema of result.data
Array<{
name: string,
runs: number,
successRate: number | null,
medianDurationMs: number,
p95DurationMs: number
}>| HTTP | Meaning |
|---|---|
200 | Query result in a tRPC result.data envelope. |
401 | Invalid dashboard read key (UNAUTHORIZED). |
500 | Configuration or database read failed (INTERNAL_SERVER_ERROR). |
/api/trpc/run.detailRead a run’s event timelinePrivate read keyInput: { id: UUID }. Use the internal runs[].id from project.snapshot, not the external runId supplied during ingestion. Events are ordered by occurrence time, then insertion time. Error and result values are recorded on their corresponding events.
An unknown run UUID, or a run belonging to a different project, returns an empty array with HTTP 200, not 404. properties contains each event's own payload; merge started/properties event patches chronologically for run context. receivedAt is when the API persisted that event.
curl --get "$FLOWMARK_API_URL/api/trpc/run.detail" \
-H "Authorization: Bearer $FLOWMARK_READ_KEY" \
--data-urlencode 'input={"id":"11111111-1111-4111-8111-111111111111"}'// Schema of result.data
Array<{
id: UUID,
type: "started" | "step" | "properties" | "error" | "operation_started" | "operation_completed" | "operation_failed" | "progress" | "completed" | "failed",
step: string | null, reason: string | null,
operationId: UUID | null, operationName: string | null,
durationMs: number | null,
completed: number | null, total: number | null, unit: string | null,
error: { code: string, message?: string, retryable?: boolean, handled?: boolean } | null,
result: Record<string, JsonValue> | null,
properties: Record<string, JsonValue> | null,
timestamp: string, receivedAt: string, workflow: string, externalId: string,
environment: string | null, release: string | null,
correlationId: string | null, parentRunId: string | null
}>| HTTP | Meaning |
|---|---|
200 | Query result in a tRPC result.data envelope. |
401 | Invalid dashboard read key (UNAUTHORIZED). |
500 | Configuration or database read failed (INTERNAL_SERVER_ERROR). |
400 | Missing input or id is not a UUID (BAD_REQUEST). |
/api/trpc/run.compareCompare a run to recent peersPrivate read keyInput: { id: UUID }. Returns the current run and the last 20 finished or recorded peers of the same workflow, plus peer success rate, median duration, and p95 duration.
curl --get "$FLOWMARK_API_URL/api/trpc/run.compare" \
-H "Authorization: Bearer $FLOWMARK_READ_KEY" \
--data-urlencode 'input={"id":"11111111-1111-4111-8111-111111111111"}'// Schema of result.data
{
current: StoredRun | null,
peers: StoredRun[],
peerSuccessRate: number | null,
peerMedianDurationMs: number | null,
peerP95DurationMs: number | null
}| HTTP | Meaning |
|---|---|
200 | Query result in a tRPC result.data envelope. |
401 | Invalid dashboard read key (UNAUTHORIZED). |
500 | Configuration or database read failed (INTERNAL_SERVER_ERROR). |
400 | Missing input or id is not a UUID (BAD_REQUEST). |
tRPC success: {"result":{"data":…}}. Errors use an error envelope containing message, a numeric code, and data.code / data.httpStatus / data.path. Batch envelopes differ from the single-call examples above.
Both ingestion routes accept the same JSON envelope, limited to 65,536 bytes (64 KiB). Send a started event before any subsequent event for a workflow/run pair.
{
"eventId": "11111111-1111-4111-8111-111111111111",
"event": {
"type": "started",
"workflow": "checkout",
"runId": "checkout-example-001",
"timestamp": "2026-01-01T12:00:00.000Z",
"properties": {
"source": "reference-example"
},
"correlationId": "checkout-request-123",
"parentRunId": "browser-run-id-from-application-propagation"
}
}| Field | Contract |
|---|---|
eventId | Required UUID. Stable identifier for deduplication; retries should reuse it. |
event.type | Required: started | step | properties | error | operation_started | operation_completed | operation_failed | progress | completed | failed. |
event.workflow | Required string, 1–200 characters, not whitespace-only. |
event.runId | Required string, 1–200 characters, not whitespace-only. An external identifier, not necessarily a UUID. |
event.timestamp | Required string parseable by Date.parse. Use an ISO 8601 timestamp with timezone. |
event.step | Required only for step: string, 1–200 characters, not whitespace-only. |
event.properties | Optional on started and step, and required for properties. Properties are JSON objects up to five nested levels, 128 keys, 128 characters per key, 2,048 characters per string, and 16 KiB encoded. Amendments append context without rewriting prior events. |
event.reason | Legacy optional description on failed, limited to 2,048 characters. Do not send secrets or sensitive personal data. |
event.failure / event.error | failed may include failure details { code, message?, retryable? }. An error event requires { code, message?, retryable?, handled } and does not terminate the run. Codes are limited to 128 characters; messages to 2,048. |
event.result | Optional bounded JSON object on completed. |
event.environment / event.release | Optional deployment metadata on started. |
event.operationId / operationName | Required for operation events. Each independent operation has its own UUID; terminal events repeat that ID and include measured durationMs. |
event.progress | Absolute snapshot with nonnegative safe-integer completed and optional total and unit. When total is present, completed must not exceed it. Values are validated, never capped. |
CORS and public browser keys are not anti-abuse authentication. Treat browser events as untrusted telemetry, not billing, authorization, or proof of a business outcome. PostgreSQL is required for ingestion and protected reads; neither health route checks database readiness.