Skip to content

http api

Every endpoint the server serves. This is generated from kayak_core::api_docs::endpoints() — the table api_router is folded over — so it describes the routes that exist and cannot describe any that don't. The same table is served as an OpenAPI 3.1 document, which a running server also renders at /api/reference with a panel you can fire calls from.

Access is the badge on each endpoint, and it is enforced by the middleware the router applies from this same entry rather than being a second fact that agrees with it today. On a server with no accounts configured, none of it applies: nobody is identified, so nothing is checked. See authentication.

Bodies link to the schemas they name, which are generated from the Rust types the handlers actually deserialize into.

pipelines

The running graph. Creating and deleting pipelines takes effect immediately and writes nothing to disk.

GET /api/pipelines

Every pipeline the server is running

read listPipelines — Any signed-in user.

The running graph, as the configs the pipelines were built from — each with the id it is actually running under, which is generated when the config omitted one.

This is the runtime's view rather than the file's: a pipeline created since startup is here and not in the config file, and GET /api/settings is what says whether the two have diverged.

responses

statusbodydescription
200[PipelineDto]The running pipelines, in no particular order.
500ApiErrorSomething went wrong on the server. The body says what.

POST /api/pipelines

Build and start a pipeline

admin createPipeline — Signed-in users with the admin role.

The body is one pipeline's config, exactly as it would appear in the pipelines array of a config file. Omitting id generates a readable random one, which comes back in the response.

The pipeline is built and started before the response is sent, so a 201 means it is running — a component that could not be built (an unknown connection, an unresolved secret) is a 422 and nothing is started. Nothing is written to disk: the config file is a load source and a save target, never a mirror of the runtime.

request bodyConfig The pipeline to build.

responses

statusbodydescription
201PipelineDtoBuilt and running, with the id it took.
409ApiErrorA pipeline with this id is already running.
422ApiErrorThe config is well-formed JSON but could not be built — an unknown connection, a missing secret, an upstream that does not exist.
500ApiErrorSomething went wrong on the server. The body says what.

DELETE /api/pipelines/{pipeline_id}

Stop and remove a pipeline

admin deletePipeline — Signed-in users with the admin role.

Cancels the pipeline's run loop and drops it from the graph. Pipelines downstream of it keep running and stop receiving from it.

Like creating one, this writes nothing to disk.

path parameters

namedescription
pipeline_idThe id the pipeline is running under.

responses

statusbodydescription
204Stopped and removed.
404ApiErrorNo pipeline is running under that id.
500ApiErrorSomething went wrong on the server. The body says what.

POST /api/pipelines/{pipeline_id}/messages

Post messages into a pipeline

public ingestMessages — Callable without credentials, even when authentication is on.

The endpoint a pipeline's http input serves. Every pipeline with one has this path, derived from its id, and it exists for as long as the pipeline is running — this is how a system pushes data into kayak without a broker in between.

The body is one JSON message or an array of them; an array arrives as a single batch, so posting ten messages is one pass through the transforms rather than ten. There is no envelope and no schema: whatever is posted is what the transforms see.

Accepted means queued, not processed. The batch is handed to the pipeline's run loop and the response is sent without waiting for the outputs, so a 202 says nothing about whether the data has landed anywhere.

This endpoint does not use the server's sign-in — it is a data plane, and a system pushing readings should not need an account that can rewrite the graph. Protecting it is the http input's own auth field: a token the sender repeats in a header, declared per pipeline. Without one the endpoint takes anything that reaches it, which is the default.

path parameters

namedescription
pipeline_idThe id of the pipeline to post to.

request bodyIngestRequest One message, or an array of messages to deliver as one batch.

responses

statusbodydescription
202IngestResponseQueued for the pipeline, with the number of messages taken.
404ApiErrorNo pipeline is running under that id, or the one that is has no http input to post to.
401ApiErrorThe input has an auth and this post didn't satisfy it. This is the input's own credential, not the server's sign-in: an account on the server does not let you post, and the token does not let you do anything else.
503ApiErrorThe pipeline's queue is full — it is not reading as fast as this is being posted. Nothing was taken; send it again.
500ApiErrorSomething went wrong on the server. The body says what.

GET /api/pipelines/{pipeline_id}/history

What a pipeline has been doing, after the fact

read getPipelineHistory — Any signed-in user.

Throughput and failures over time, kept in the server's memory so that something which broke overnight can still be read in the morning.

This is the counterpart to /events, not a replay of it. The event stream is a live sample: it is only produced while a browser is attached and it drops passes under load on purpose. History is fed by counters the run loop keeps regardless of who is watching, so it is complete in what it counts — and correspondingly it carries no message payloads at all, only counts, and failures aggregated to one entry per distinct message with a first-seen, a last-seen and a tally.

Buckets are contiguous and oldest first, including empty ones: a run of zeroes is a pipeline that stopped, which is a different fact from a gap and is spelled differently.

An unknown or newly created pipeline answers with an empty history rather than a 404 — a pipeline that has not done anything yet is not an error. How much is kept is the history.retention_secs in the server config; when that is zero nothing is recorded and this always answers empty.

path parameters

namedescription
pipeline_idId of the pipeline.

query parameters — all optional, so a bare request to this path is a working request.

namedescription
resolutioncoarse (the default) — a minute a bucket, over the configured retention, which is the overnight record. fine — five seconds a bucket over the last half hour, which is what a card's live chart is backfilled from so it starts full rather than drawing itself over the next two minutes.

responses

statusbodydescription
200PipelineHistoryThe pipeline's history at the resolution asked for.

POST /api/scripts/dry-run

Run a script over some messages, without creating a pipeline

admin dryRunScript — Signed-in users with the admin role.

A script transform is the one component whose configuration can be wrong in a way the config's shape cannot express: for every other component, a config that deserializes and builds does what it says, and for this one the interesting mistakes are all inside a string. This endpoint is where that string gets checked.

It compiles the script and runs it over the messages in the body, through the same runner and under the same operation budget and sandbox a running transform gets — a dry run that could disagree with production would be worse than none, because it would be trusted.

A script with a bug in it is a 200, not a 400. The request was well formed and the server answered it completely; where the bug is is the answer. The response is a tagged union: emitted carries the batches, failed carries the message with a line and column an editor can point at. A 400 here means the request itself was wrong — malformed JSON, or a file source naming something unreadable.

State is never live. The run gets a private bucket seeded from state in the body and thrown away afterwards, and what it holds at the end comes back in the response. Reading production state would make the answer depend on what the server happened to be doing; writing it would give a dry run side effects.

request bodyDryRunRequest The script, and the messages to run it over.

responses

statusbodydescription
200DryRunResponseThe script ran, or it did not compile — the outcome field says which.
400ApiErrorThe request itself was wrong: malformed JSON, or a file source that could not be read.
500ApiErrorSomething went wrong on the server. The body says what.

POST /api/inputs/sample

Fetch a few real messages from an input, without creating a pipeline

admin sampleInput — Signed-in users with the admin role.

Configuring a stream you cannot see is guesswork, and every field reference downstream — a column's field, a filter's comparison — is a name someone had to already know. This builds the input in the body exactly as a pipeline would, takes up to max_messages from it within timeout_ms, and drops it.

The real input, including its envelope, so the metadata fields the messages will actually carry are in the sample too. Its buffer is the one thing ignored: a buffer's job is to make the pipeline wait, which is not what a sample is for. Anything the sample did differently comes back in notes.

Sampling is not free for every kind of input, and the ones where it isn't say so. A kafka sample runs under a throwaway consumer group, so it neither rebalances the pipeline's group nor commits on its behalf; an mqtt sample connects under a client id of its own, because a broker disconnects the older client holding one. An http input is refused outright with a 400 — it is posted to rather than read from, so there is nothing to fetch.

No messages is a 200 with an empty list. A subject nobody is publishing to is a real state of the world and the answer to the question asked; none of these inputs can replay what was published before the sample started.

request bodySampleRequest The input to read from, and how much to take.

responses

statusbodydescription
200SampleResponseThe sample was taken — outcome says whether it produced messages or failed on the way.
400ApiErrorThe request itself was wrong: malformed JSON, an input that isn't a kind of input, or one that cannot be sampled at all.
500ApiErrorSomething went wrong on the server. The body says what.

POST /api/pipelines/dry-run

Run a draft's transforms over some messages, without creating a pipeline

admin dryRunPipeline — Signed-in users with the admin role.

What a map writes, what a filter drops, what a reduce collapses a batch to — questions the config cannot answer and one real message can. This builds the transforms in the body exactly as a pipeline would and puts the messages down the chain, reporting what each stage handed on.

Per stage and as a list of batches, because that is where the answer usually is: a splitter hands on several batches, a filter that dropped everything hands on none, and a buffer hands on nothing because it is still holding what it was given. What a transform only releases when the chain is drained is reported separately, as on_flush, so a buffer doesn't look like it passes everything straight through. A transform that is still holding what it was given hands on nothing at all, and the chain says so rather than pretending the messages came through: a dry run has no tick to give a window that has thirty seconds left on it.

There are no outputs and there cannot be. A dry run that emitted would be a pipeline; everything up to the outputs is a question about the data, and the outputs are the part that changes somebody else's system.

State is never live, exactly as for a script dry run: the buckets are private to the request, seeded from buckets in the body, returned in the response and thrown away with it.

A transform that cannot be built, or that fails on a message, is a 200 whose outcome is failed — the request was carried out and where it broke is the answer. The stages that completed first come back with it.

request bodyPipelineDryRunRequest The messages, and the transforms to put them through.

responses

statusbodydescription
200PipelineDryRunResponseThe chain ran, or it broke on the way — the outcome field says which.
400ApiErrorThe request itself was wrong: malformed JSON, or a transform that isn't a kind of transform.
500ApiErrorSomething went wrong on the server. The body says what.

connections

The systems pipelines talk to, named once and referred to by the components that use them.

GET /api/connections

The connections pipelines can name

read listConnections — Any signed-in user.

Keyed by name, which is the same shape as the connections file itself — what the UI lists and what gets committed are one thing, so there is no second format to keep in step.

Credentials come back as the unresolved ${NAME} templates they are configured as, never as their values.

responses

statusbodydescription
200ConnectionsEvery configured connection, in name order.

POST /api/connections

Add a connection

admin createConnection — Signed-in users with the admin role.

Changes what the next pipeline build can name, and nothing else: a component reads its connection once, when it is built, so editing or adding one reaches only new and rebuilt pipelines.

Like creating a pipeline this writes nothing to disk; the save does, and it writes the config and the connections file together.

request bodyCreateConnectionRequest The connection, and the name to file it under.

responses

statusbodydescription
201CreateConnectionRequestAdded, echoed back as stored.
409ApiErrorA connection of that name already exists.
500ApiErrorSomething went wrong on the server. The body says what.

DELETE /api/connections/{connection_id}

Remove a connection

admin deleteConnection — Signed-in users with the admin role.

Refused while a running pipeline still names it — that comes back as a 409 listing the pipelines, so the answer says what to do about it.

path parameters

namedescription
connection_idThe name the connection is filed under.

responses

statusbodydescription
204Removed.
404ApiErrorNo connection of that name exists.
409ApiErrorRunning pipelines still name it; the body lists them.
500ApiErrorSomething went wrong on the server. The body says what.

state

What the pipelines remember between batches. Read-only: buckets are declared in the config and filled by remember transforms, so there is nothing here to write.

GET /api/state

The state buckets and how full they are

read listStateBuckets — Any signed-in user.

One entry per bucket declared under state in the config, in name order, with the number of keys it is currently holding and the bounds it is held to.

Buckets are not created or deleted through the API — they are part of the graph's logic and live in the config file, so this family is read-only.

responses

statusbodydescription
200BucketSummaryEvery declared bucket, in name order.

GET /api/state/{bucket}

What one bucket is holding

read getStateBucket — Any signed-in user.

The keys and the values remembered under each, most recently written first — which is the order that makes a live bucket readable, since the key that just changed is the one worth seeing.

Capped: a bucket may hold thousands of keys and this returns a page of them, with truncated saying so and keys giving the real total. It is a snapshot taken under the bucket's lock, so it is consistent with itself and stale the moment it is sent.

path parameters

namedescription
bucketName of the bucket, as declared in the config.

responses

statusbodydescription
200BucketContentsThe bucket's contents.
404ApiErrorNo bucket of that name is declared.

config

The config file: how the server was started, writing the running graph out to it, and throwing the graph away to start again from it.

GET /api/settings

How the server was started, and whether it has drifted

read getSettings — Any signed-in user.

Which config file the server is working against, where a save would land, and whether the running graph has diverged from what was last loaded or saved.

The absence of a config file doesn't mean edits can't be saved: it means there is no file yet, and a save creates one.

responses

statusbodydescription
200SettingsDtoThe server's configuration state.

POST /api/config/save

Write the running graph to a config file

admin saveConfig — Signed-in users with the admin role.

Writes the running pipelines out in a deterministic order (topological, ties by id) via a temp file and a rename, because the result is meant to be committed. The connections file and the canvas layout are written beside it by the same save — a config saved without the connections it names would not start.

name is a bare file name, not a path, and is validated as one: the file lands in the server's save directory and nowhere else. Using the loaded file's own name is how you overwrite it.

format picks JSON or YAML; leaving it out takes the format from the name's extension. On a server started without --config this is how a config file comes into existence at all, and from that save on it is the file revert reloads.

overwrite defaults to true, which is what makes saving over the loaded file the ordinary thing it has always been. Sending false turns the save into a create: if the name — or either of the two files written beside it — is already on disk, the request is refused with a 409 and nothing is written. That is what the UI's project creator sends, since it suggests a file name into a directory its user has often never looked at.

request bodySaveConfigRequest The file name to write, optionally the format, and whether an existing file may be replaced.

responses

statusbodydescription
200SaveConfigResponseWritten, with the path it landed at.
409ApiErroroverwrite was false and the file — or one of the two written beside it — is already there. Nothing was written; the message names the files.
422ApiErrorname is not a bare file name.
500ApiErrorSomething went wrong on the server. The body says what.

POST /api/config/revert

Throw the running graph away and reload the config file

admin revertConfig — Signed-in users with the admin role.

The undo for a session of editing, and as destructive as it sounds: every running pipeline is stopped and the graph is rebuilt from the file.

The file is parsed before the runtime is torn down, so a file broken by hand costs you nothing. The connections are reloaded first, since the pipelines being rebuilt name them. It waits for the old pipelines to actually stop before rebuilding, so the response landing means the new graph is the only one running.

responses

statusbodydescription
204Reloaded; the graph is what the file says.
500ApiErrorThere is no config file to revert to, or it could not be read or parsed — in which case the running graph is left alone.

layout

Where the cards sit on the canvas. Not configuration — this is written to its own file, and immediately.

GET /api/layout

Where the cards sit on the canvas

read getLayout — Any signed-in user.

Served separately from /api/pipelines because it is a different kind of thing: that is what the server is running, this is how someone chose to look at it. A client that ignores this endpoint gets an automatically laid out graph, which is the point.

Only pipelines someone has actually moved appear.

responses

statusbodydescription
200LayoutFileThe stored arrangement.

PUT /api/layout

Replace the arrangement and write it to disk

admin replaceLayout — Signed-in users with the admin role.

The whole map, not a patch: the canvas already holds the complete arrangement, and a full replacement is what makes "reset everything to automatic" a send of {} rather than its own endpoint.

This is the one edit that writes immediately rather than waiting for a save, and it never counts as an unsaved change — moving a card changes nothing the server runs, so there is nothing worth reviewing before it lands. Without a config file there is nowhere to write, and the arrangement is kept in memory until a save creates one.

request bodyLayoutFile The complete arrangement.

responses

statusbodydescription
204Stored, and written if there is a file to write.
500ApiErrorSomething went wrong on the server. The body says what.

events

What the pipelines are doing, as it happens.

GET /events

What the pipelines are doing, as it happens

read streamEvents — Any signed-in user.

A text/event-stream of UiEvents: a batch arriving at a stage, or a failure handling one. Each SSE data: field is one event as JSON.

It is a broadcast that drops rather than blocks, so a slow consumer misses events instead of slowing the pipelines down — which is what seq is for, since a gap in it is the honest report of what was missed. Run loops only publish at all while somebody is listening.

The stream is explicitly a dev-tooling affordance rather than a durable feed, and is marked temporary in the source.

responses

statusbodydescription
200UiEventAn event stream that stays open.

auth

Signing in and out. Present on every server; on one with no accounts configured they report that there is nothing to sign into.

GET /api/auth/me

Who the caller is, and whether this server asks

public whoAmI — Callable without credentials, even when authentication is on.

authentication_required says whether this server checks credentials at all — a server started without a --server-config, or with one declaring auth: {type: none}, answers false and lets everybody do everything.

username and role describe the caller, and are both null for one who presented nothing. Note that a null role is not the same as read: a reader may see the graph, a signed-out caller may not.

Callable without credentials, necessarily — it is the endpoint that answers 'do I need to show a login page'.

responses

statusbodydescription
200AuthDtoWho you are. Not an error even when the answer is nobody.

POST /api/auth/login

Exchange credentials for a session

public login — Callable without credentials, even when authentication is on.

Checks a username and password against the accounts in the server's settings file and, on success, sets an HttpOnly session cookie.

This is for browsers. Everything else should send Authorization: Basic on each request instead and never come here — the cookie exists because EventSource, which the UI consumes /events with, cannot send headers.

A wrong password and an unknown username are the same 401, deliberately: the endpoint is not a way to find out who has an account. On a server with no accounts configured this is not an error either — it answers 200 with authentication_required false, because there is nothing to sign into.

request bodyLoginRequest The credentials to check.

responses

statusbodydescription
200AuthDtoSigned in. The session cookie is in Set-Cookie.
401ApiErrorWrong username or password — the body does not say which.
500ApiErrorSomething went wrong on the server. The body says what.

POST /api/auth/token

Exchange an identity provider's JWT for a session

public tokenLogin — Callable without credentials, even when authentication is on.

The embedding flow's endpoint, on a server whose auth section is jwt: a host application that already holds a token from the shared identity provider — Cognito, Keycloak — puts it on the iframe URL as ?auth_token=, and the UI posts it here once. The token is checked against the issuer's published keys and, on success, exchanged for the same HttpOnly session cookie a password login sets — so the token itself appears in exactly one request and never in an access log again.

The session ends no later than the token's exp: the cookie must not outlive the identity provider's word that the caller is signed in.

API callers don't need this exchange — on a jwt server, Authorization: Bearer <token> works directly on every endpoint.

Every way of being refused is the same 401, deliberately: an expired token, a wrong issuer and a server that doesn't take tokens at all are not distinctions worth handing to a guesser.

request bodyTokenLoginRequest The token to check.

responses

statusbodydescription
200AuthDtoSigned in. The session cookie is in Set-Cookie.
401ApiErrorThe token was not accepted, or this server does not take tokens.
500ApiErrorSomething went wrong on the server. The body says what.

POST /api/auth/logout

End the session this request carries

read logout — Any signed-in user.

Clears the cookie in the browser and drops the session on the server, so a copy of the cookie taken from somewhere else stops working too.

Idempotent: 204 whether or not there was a session to end.

responses

statusbodydescription
204Signed out.

reference

The API describing itself.

GET /api/docs

The component reference, as data

public listComponents — Callable without credentials, even when authentication is on.

Every input, transform, output and connection kayak can build, with their fields, types and documentation — reflected out of the config schemas, so it cannot drift from what the server actually accepts.

The /docs page generates the same thing in the browser from the same code, so this endpoint isn't what renders it. It exists because the component reference is useful to things that aren't a browser: a config linter, editor completion, a test.

responses

statusbodydescription
200[ComponentDoc]Every component, grouped by nothing — family says which plugin point each one plugs into.

GET /api/openapi.json

This API, as an OpenAPI 3.1 document

public getOpenApi — Callable without credentials, even when authentication is on.

Generated from the same table the routes are registered from, with schemas reflected out of the Rust types — so it describes the server that is serving it.

Point a renderer, a client generator or a contract test at it. GET /api/reference is one such renderer, served alongside.

responses

statusbodydescription
200OpenApiDocumentThe OpenAPI document.

GET /api/reference

The rendered API reference

public apiReference — Callable without credentials, even when authentication is on.

An HTML page rendering /api/openapi.json, with a request panel for trying endpoints against this server.

The /docs page in the UI covers the same endpoints in kayak's own furniture; this is the full reference, schemas and all.

responses

statusbodydescription
200htmlThe reference page.

reference tables generated from the config schemas — just docs