Skip to API reference
Flowmark / API reference

Developer reference v1 ingestion · tRPC queries

Your workflows.
One observable timeline.

Send explicit workflow events, inspect project activity, and follow a run from start to outcome. Everything the current API exposes, in one place.

API ORIGINhttps://api.flowmark.devJSON over HTTP
01

Separate keys. Separate trust.

Protected 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.

SERVER → API

Private write key

Ingest trusted server events with /v1/events. Never expose this credential in a browser.

SERVER → API

Private read key

Read project data through tRPC. Keep the key behind a same-origin dashboard proxy.

BROWSER → API

Public browser key

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.

02

Endpoints & queries

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.

GET · readPOST · ingestOPTIONS · preflightKeyboard: Tab + Enter to expand
GET/api/healthCheck API livenessNo key

A database-independent liveness check. This confirms the HTTP service responds, not that PostgreSQL or ingestion is ready.

Request example

curl "$FLOWMARK_API_URL/api/health"

Success response / schema

{"status":"ok","service":"flowmark-api"}

Response codes

Responses for /api/health
HTTPMeaning
200The API HTTP service is alive.
POST/v1/eventsIngest a server workflow eventPrivate write key

Accepts 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.

Request example

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"
  }
}'

Success response / schema

{"accepted":true}

Response codes

Responses for /v1/events
HTTPMeaning
202Persisted, or duplicate event ID ignored. {"accepted":true}
400Invalid JSON, invalid event envelope, or body over 64 KiB.
401Missing, invalid, or wrong-purpose Bearer key.
409Send the started event before other run events.
503Configuration or event storage unavailable.
POST/v1/browser-eventsIngest an untrusted browser eventPublic browser key + Origin

Uses 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.

Request example

// 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"
  }
}),
});

Success response / schema

{"accepted":true}

Response codes

Responses for /v1/browser-events
HTTPMeaning
202Persisted, or duplicate event ID ignored. {"accepted":true}
400Invalid JSON, invalid event envelope, or body over 64 KiB.
401Missing, invalid, or wrong-purpose Bearer key.
403Missing Origin, or origin not allowed for this browser key.
409Send the started event before other run events.
429120 events/minute/key exceeded; Retry-After: 60.
503Configuration or event storage unavailable.
OPTIONS/v1/browser-eventsNegotiate browser ingestion CORSAllowed Origin

Preflight 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.

Request example

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"

Success response / schema

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

Response codes

Responses for /v1/browser-events
HTTPMeaning
204Allowed origin and POST requested.
403Missing/disallowed origin or requested method is not POST.
503Configuration or origin lookup unavailable.
OPTIONS/api/trpc/{procedure}Typed query transport · GET / POST / OPTIONSDepends on procedure

The 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.

Request example

curl -i -X OPTIONS "$FLOWMARK_API_URL/api/trpc/project.snapshot" \
  -H "Origin: https://app.flowmark.dev" \
  -H "Access-Control-Request-Method: GET"

Success response / schema

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

Response codes

Responses for /api/trpc/{procedure}
HTTPMeaning
204OPTIONS returns no body; unrecognized origins get no CORS grant.
400Malformed input or invalid run UUID (BAD_REQUEST).
404Unknown procedure (NOT_FOUND).
405Wrong method, including POST to a query (METHOD_NOT_SUPPORTED).
415Unsupported or missing POST Content-Type (UNSUPPORTED_MEDIA_TYPE).
GET/api/trpc/health.checkCheck liveness through tRPCNo key

The typed equivalent of the HTTP health check. No input, authentication, or database access.

Request example

curl "$FLOWMARK_API_URL/api/trpc/health.check"

Success response / schema

{"result":{"data":{"status":"ok","service":"flowmark-api"}}}

Response codes

Responses for /api/trpc/health.check
HTTPMeaning
200Service responds with a tRPC result envelope.
GET/api/trpc/project.snapshotRead the project overviewPrivate read key

No 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.

Request example

curl "$FLOWMARK_API_URL/api/trpc/project.snapshot" \
  -H "Authorization: Bearer $FLOWMARK_READ_KEY"

Success response / schema

// 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 }>
}

Response codes

Responses for /api/trpc/project.snapshot
HTTPMeaning
200Query result in a tRPC result.data envelope.
401Invalid dashboard read key (UNAUTHORIZED).
500Configuration or database read failed (INTERNAL_SERVER_ERROR).
GET/api/trpc/workflow.listList workflow summariesPrivate read key

No input. Returns the workflows portion of the authorized project snapshot, ordered by workflow name.

Request example

curl "$FLOWMARK_API_URL/api/trpc/workflow.list" \
  -H "Authorization: Bearer $FLOWMARK_READ_KEY"

Success response / schema

// Schema of result.data
Array<{
  name: string,
  runs: number,
  successRate: number | null,
  medianDurationMs: number,
  p95DurationMs: number
}>

Response codes

Responses for /api/trpc/workflow.list
HTTPMeaning
200Query result in a tRPC result.data envelope.
401Invalid dashboard read key (UNAUTHORIZED).
500Configuration or database read failed (INTERNAL_SERVER_ERROR).
GET/api/trpc/run.detailRead a run’s event timelinePrivate read key

Input: { 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.

Request example

curl --get "$FLOWMARK_API_URL/api/trpc/run.detail" \
  -H "Authorization: Bearer $FLOWMARK_READ_KEY" \
  --data-urlencode 'input={"id":"11111111-1111-4111-8111-111111111111"}'

Success response / schema

// 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
}>

Response codes

Responses for /api/trpc/run.detail
HTTPMeaning
200Query result in a tRPC result.data envelope.
401Invalid dashboard read key (UNAUTHORIZED).
500Configuration or database read failed (INTERNAL_SERVER_ERROR).
400Missing input or id is not a UUID (BAD_REQUEST).
GET/api/trpc/run.compareCompare a run to recent peersPrivate read key

Input: { 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.

Request example

curl --get "$FLOWMARK_API_URL/api/trpc/run.compare" \
  -H "Authorization: Bearer $FLOWMARK_READ_KEY" \
  --data-urlencode 'input={"id":"11111111-1111-4111-8111-111111111111"}'

Success response / schema

// Schema of result.data
{
  current: StoredRun | null,
  peers: StoredRun[],
  peerSuccessRate: number | null,
  peerMedianDurationMs: number | null,
  peerP95DurationMs: number | null
}

Response codes

Responses for /api/trpc/run.compare
HTTPMeaning
200Query result in a tRPC result.data envelope.
401Invalid dashboard read key (UNAUTHORIZED).
500Configuration or database read failed (INTERNAL_SERVER_ERROR).
400Missing 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.

03

One envelope. Ten event types.

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"
  }
}
EventEnvelope · required and variant fields
FieldContract
eventIdRequired UUID. Stable identifier for deduplication; retries should reuse it.
event.typeRequired: started | step | properties | error | operation_started | operation_completed | operation_failed | progress | completed | failed.
event.workflowRequired string, 1–200 characters, not whitespace-only.
event.runIdRequired string, 1–200 characters, not whitespace-only. An external identifier, not necessarily a UUID.
event.timestampRequired string parseable by Date.parse. Use an ISO 8601 timestamp with timezone.
event.stepRequired only for step: string, 1–200 characters, not whitespace-only.
event.propertiesOptional 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.reasonLegacy optional description on failed, limited to 2,048 characters. Do not send secrets or sensitive personal data.
event.failure / event.errorfailed 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.resultOptional bounded JSON object on completed.
event.environment / event.releaseOptional deployment metadata on started.
event.operationId / operationNameRequired for operation events. Each independent operation has its own UUID; terminal events repeat that ID and include measured durationMs.
event.progressAbsolute 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.