REST API preview
Call the authenticated tapstate control-plane endpoints and interpret preview responses
The preview server exposes an HTTP control plane. This API exists only after a separate tapstate server is running; installing the CLI does not start the server or expose an HTTP port. The server is headless and has no built-in web UI in this release.
The API has no stable OpenAPI publication or versioned URL namespace yet; pin the tapstate release and treat request and response compatibility as prerelease.
Prefer the CLI for interactive use. Use the REST surface when building a controlled integration that can handle authentication, coded errors, synchronous connector operations, and contract changes.
Base URLs and authentication
Unauthenticated endpoints:
| Method | Path | Purpose |
|---|---|---|
GET | /healthz | Process liveness only; returns ok. |
GET | /version | Report the server release and its versioned contracts. |
POST | /auth/bootstrap | Create the first administrator from loopback, once. |
POST | /auth/login | Exchange username/password for a short-lived bearer token; it can also create a revocable user session. |
Read the server version
GET /version is available before authentication so a client can identify the
server before it logs in or reasons about supported operations:
curl -sS http://127.0.0.1:8080/version{
"version": "<server-version>",
"dslVersions": [],
"dataVersion": null
}version identifies the server release. dslVersions is reserved for the
authoring grammar versions the server accepts, and dataVersion is reserved
for the schema version of its system data. The current response keeps the
reserved fields present even when they have no value; do not treat an empty
list or null as a missing field.
Use /healthz only for process liveness. It returns ok and does not identify
the running build.
Every /api/** endpoint requires:
Authorization: Bearer <token>See Bootstrap and authenticate for the request sequence.
Create and use a user session
| Method | Path | Purpose |
|---|---|---|
POST | /auth/session | Exchange a saved user session for a fresh bearer token. |
POST | /auth/logout | Revoke a saved user session. |
POST /auth/login remains compatible with callers that send only username
and password: its token is the short-lived bearer credential for /api/**.
Set createSession to true only when the client needs a revocable user
session that it can later exchange for fresh bearer credentials:
{
"username": "admin",
"password": "<password>",
"createSession": true
}When requested, the response also includes sessionToken,
sessionIdleExpiresAt, and sessionAbsoluteExpiresAt. Keep the session token
secret. It is not a Bearer token and must not be sent to /api/**.
To obtain a fresh access token, send the session token to POST /auth/session:
Authorization: TapstateSession <sessionToken>The response has the ordinary access-token fields (token, accessExpiresAt,
issuer, principal, and scopes); use its token as the Bearer value for
/api/**. Send the same TapstateSession header to POST /auth/logout to
revoke the session. A successful logout returns HTTP 204.
Artifact endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/artifacts:apply | Parse, validate, and apply a batch of raw YAML drafts. |
POST | /api/artifacts:validate | Parse and validate a batch of raw YAML drafts without storing them. |
GET | /api/artifacts | List stored artifacts; optional kind query parameter. |
GET | /api/artifacts/{id} | Read one stored artifact. |
DELETE | /api/artifacts/{id} | Permanently remove one stored artifact; requires If-Match with the current content hash. |
Artifact deletion requires a quoted 64-character content hash from the artifact's current representation:
If-Match: "<64-hex-content-hash>"Read the artifact first to obtain the hash. The contentHash field is the
unquoted 64-character value returned by GET /api/artifacts/{id} (and by the
artifact list); wrap that value in quotes only when forming the If-Match
header:
{
"id": "orders_source",
"kind": "source",
"canonicalForm": "...",
"contentHash": "<64-hex-content-hash>"
}The server returns 428 when the header is missing or malformed, 412 when the hash is stale, and 204 after the artifact is removed.
Apply request:
{
"drafts": [
{
"source": "source/orders_source.tap.yml",
"content": "version: tapstate/v1\nkind: source\nid: orders_source\nconnector: mysql\nconfig:\n host: mysql.invalid\n port: \"3306\"\n database: appdb\n username: tapstate\nmode: snapshot\n"
}
]
}The server reparses the YAML and returns one CREATED, UPDATED, or UNCHANGED outcome per draft.
Apply can return dsl.upsert-needs-key after source discovery. Its params
identify the affected source, table, and resource path. Select a source
table with a discovered primary key, or use append only for insert-only
delivery. A unique index, target-side key, or tables[].pk does not satisfy the
current runtime check.
Connector and connection endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/connectors:register | Register a base64-encoded connector JAR. |
GET | /api/connectors | List the bundled and registered online catalog. |
GET | /api/connectors/{id} | Read one connector's normalized fields and capabilities. |
POST | /api/connections:test | Run and store a connector connection test synchronously. |
GET | /api/connections/{id}/test-result | Read the latest stored test report. |
POST | /api/connections:discover-schema | Run and store source discovery synchronously. |
GET | /api/connections/{id}/schema | Read the latest stored discovered model. |
Use tapstate register instead of manually base64-encoding large JARs when possible.
Connection test request:
{
"id": "orders_source",
"connectorId": "mysql",
"settings": {
"deploymentMode": "standalone",
"host": "mysql.invalid",
"port": "3306",
"database": "appdb",
"username": "tapstate"
}
}A connector that completes its checks normally returns HTTP 200 even when the report has outcome: "FAILED". Inspect the outcome and checks. A connector that throws during testing returns a coded error response instead.
Connection test and discovery execute synchronously and can take longer than ordinary reads. Configure the caller and any reverse proxy with an appropriate timeout, and do not retry a write blindly.
Source endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/sources:draft | Validate a structured Source draft and render canonical YAML without storing it. |
POST | /api/sources | Create one structured source. |
GET | /api/sources | List structured sources. |
GET | /api/sources/{id} | Read a secret-redacted source and its ETag. |
PUT | /api/sources/{id} | Replace a source using If-Match. |
DELETE | /api/sources/{id} | Delete a source using If-Match. |
POST /api/sources:draft is an authoring check, not a create operation. It
returns a redacted Source view and canonical YAML only when the live connector
catalog can supply the complete connector contract; it creates no artifact or
audit record.
Update and delete require the quoted 64-character content hash returned in ETag. Missing or stale preconditions return HTTP 428 or 412. Secret values are not returned; use the structured secret-clearing contract deliberately when replacing a source.
Use one write model consistently. Prefer /api/artifacts:apply for resource
authoring and pipeline batches. Use the structured source endpoints when a
client needs source-specific draft and ETag workflows.
Read data through a declared source
The data-browser endpoints inspect the data behind an existing source. Each request resolves the source's configured connection; callers cannot supply a database URI, database name, or database-native query command.
Data access boundary: These endpoints are read-scoped, but they can return source records and do not create audit events in this preview. There is no separate data-browser permission, so any authenticated caller with read access can use them. Keep the REST control plane off untrusted networks, use HTTPS for remote access, and issue a dedicated read-scoped token to each integration.
| Method | Path | Purpose |
|---|---|---|
GET | /api/sources/{sourceId}/collections | List collections held by a declared source's database. |
GET | /api/sources/{sourceId}/collections/{collection}/stats | Read connector-reported metadata for one collection. |
POST | /api/sources/{sourceId}/collections/{collection}:find | Read a bounded preview of rows from one collection. |
Collection listing returns the database's actual collections, not only
resources declared in a workspace. The stats response can include
numOfRows, storageSize, and avgObjSize; unavailable values are not zero.
numOfRows is an estimate from connector metadata rather than a counted total.
The find request body is optional. It can contain filter, sort, and
limit:
{
"filter": {
"field": "status",
"op": "eq",
"value": "paid"
},
"sort": {
"field": "total",
"dir": "desc"
},
"limit": 25
}filter is either one {field, op, value} term or one all or any
combination of terms. Supported operators are eq, ne, gt, gte, lt,
lte, in, exists, and contains. in requires a non-empty list,
exists requires a Boolean value, and contains requires text. The API rejects
database-native operators and arbitrary nested Boolean groups.
Use asc or desc for sort.dir. A literal dotted field name must escape the
dot in the JSON value, for example "price\\.usd". Such a field can be
filtered but cannot be sorted; the server returns
data-browser.unorderable-field.
find is a one-shot preview, not a paginated query. The default is 10 rows and
the maximum is 200. The response includes rows, approximateTotal, and
moreAvailable; it has no cursor or next-page token. approximateTotal might
be unavailable for a filtered read.
In this preview, row previews are available only for sources that use the
mongodb connector. Collection listing and statistics can still be available
for other connectors. A row read against another connector returns
data-browser.connector-not-browsable.
Stream collection changes
Connect to this WebSocket endpoint to stream changes from one collection:
/api/data-browser/{source}/{collection}/tailThe WebSocket handshake requires the same bearer credential as other /api
requests. To filter the stream, provide the structured filter as URL-encoded
JSON in the filter query parameter. Each message contains a kind of
INSERT, UPDATE, or DELETE, an at timestamp, and any available before
or after row.
The stream is supported only for MongoDB sources in this preview. It reports
changes as they reach the store, so rapid intermediate updates might not appear
as separate messages. A stream with no displayed change for 10 minutes closes
with data-browser.follow-idle; a deleted source or a connector stream failure
also ends the connection with a coded reason.
Machine-token endpoints
These endpoints require an administrator credential:
| Method | Path | Purpose |
|---|---|---|
POST | /api/tokens | Create a machine token with read, write, or admin scope. |
GET | /api/tokens | List secret-free token descriptors. |
POST | /api/tokens/{id}:revoke | Revoke a machine token immediately. |
The create response returns the bearer value exactly once. Store it in a secret
manager or process environment; later list responses do not return it. Scope is
ordered read < write < admin, and a token can call operations at or below
its own grade.
Create a write-scoped token with an administrator credential:
curl --fail-with-body --silent --show-error \
-X POST "$TAPSTATE_URL/api/tokens" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
--data '{"scope":"write"}'The response has this shape. The token value is returned only in this create
response:
{
"tokenId": "<token-id>",
"scope": "WRITE",
"token": "<one-time-bearer-value>",
"createdAt": "<timestamp>"
}List secret-free token metadata with GET /api/tokens. Revoke a token with
POST /api/tokens/<token-id>:revoke; revocation returns HTTP 204 and takes
effect immediately.
Pipeline lifecycle endpoints
| Method | Path |
|---|---|
POST | /api/pipelines/{id}:start |
POST | /api/pipelines/{id}:stop |
POST | /api/pipelines/{id}:pause |
POST | /api/pipelines/{id}:resume |
Each response is desired state:
{
"pipelineId": "orders_to_operational_state",
"targetState": "RUNNING",
"revision": "<content-hash>"
}The response proves that the intent was accepted. Read observed status and destination data before reporting execution success.
Observation endpoints
| Method | Path | Boundary |
|---|---|---|
GET | /api/pipelines/{id}/status | Store-backed observed lifecycle state |
GET | /api/pipelines/{id}/metrics | Open metric map and optional perTableOffset |
GET | /api/pipelines/{id}/snapshot | Per-table initial-load progress when available |
GET | /api/pipelines/{id}/logs | Bounded node-local log tail |
The status response includes pipelineId and state. When the observed state
records a job failure, it also includes a failure object with code,
params, and a reader-facing message. The same shape is used in status watch
frames. The failure object is omitted when there is no failure to report.
WebSocket endpoints:
| Path | Behavior |
|---|---|
/api/pipelines/{id}/status/watch | Emits status when the observed state changes. |
/api/pipelines/{id}/logs/follow | Emits newly observed node-local log lines. |
The WebSocket handshake uses the same bearer header. It returns HTTP 401 or 403 instead of upgrading when authentication or authorization fails.
Cluster endpoint:
| Method | Path | Boundary |
|---|---|---|
GET | /api/cluster/members | Present but returns HTTP 501 in the current preview |
Error responses
Coded failures use:
{
"code": "lifecycle.illegal-transition",
"params": {
"from": "RUNNING",
"verb": "start"
},
"message": "..."
}Treat code as the stable machine key. message is reader-facing and can change.
engine.job-failed is a pipeline failure code, not a request-level HTTP error.
When it appears as failure.code in a status response or status watch frame, it
means that the pipeline stopped because its job failed. Inspect
failure.message and the pipeline logs for the underlying cause.
Common status mappings:
| HTTP status | Typical meaning |
|---|---|
400 | Malformed JSON, DSL validation failure, or a request rejected as bad input |
401 | Missing, invalid, expired, or rejected credential |
403 | Authenticated but insufficient scope |
404 | Artifact, source, connector, pipeline, test result, schema, or observation not found |
409 | Closed bootstrap, an illegal/incompatible lifecycle state, or an artifact refusal such as artifact.in-use |
412 | Source or artifact If-Match content hash is stale |
428 | Source or artifact update/delete omitted a valid precondition |
500 | Coded server or connector failure without a client-attributable mapping |
501 | Endpoint is present but not implemented |
Artifact errors
Artifact edits and deletions use a shared artifact.* error domain:
The GET /api/artifacts/{id} read path currently returns HTTP 404
without a structured error body when the ID is absent. Handle the HTTP status
first and do not require an artifact.not-found JSON envelope for that read;
the coded error is used by artifact mutation/precondition paths.
| Code | Meaning |
|---|---|
artifact.not-found | The requested artifact does not exist. A delete that already succeeded returns this code if it is attempted again. |
artifact.precondition-required | The request did not provide a valid version precondition (428). |
artifact.version-conflict | The supplied content hash is stale (412). |
artifact.in-use | Another resource still refers to the artifact (409). |
artifact.pipeline-not-stopped | The artifact's pipeline is not stopped, so removal is refused (409). |
artifact.reclaim-incomplete | The deletion succeeded, but dependent cleanup did not finish. Do not retry the deletion. Record the reason and residue parameters and contact the server operator; a retry returns artifact.not-found. |
Unexpected server errors can return HTTP 500 without the coded envelope.
Preserve the status, response body, method, path, and tapstate version when
reporting one.
Version boundary
- The preview supports both interactive connected sessions and one-shot connected
commands seeded with
-cand-u, plus explicit creation and exchange of a revocable user session. - No
v1API prefix or compatibility guarantee exists yet.