HTTP API
The web interface has no privileged path into Syncle — everything it does goes through the REST API on this page, so anything you can click, you can script. Every route lives under /api, requests and responses are JSON, and a session cookie is the only authentication.
Conventions
The origin that serves the interface serves the API too: the web app proxies /api through to the API server, so with a default install the base URL is http://localhost:3002/api. The API container itself is not published outside the Docker network unless you expose it, so go through the web origin — the installation page covers the ports.
Every successful response is wrapped in a data envelope; every error is an error object with a machine-readable code, a human message, and a details field that is null unless the error carries extra data:
$ curl http://localhost:3002/api/health
{"data":{"ok":true}}
$ curl http://localhost:3002/api/connections
{"error":{"code":"UNAUTHORIZED","message":"Authentication required","details":null}}Scripts should branch on the code, not the HTTP status alone — two codes share status 400:
| Code | Status | Meaning |
|---|---|---|
BAD_REQUEST | 400 | Invalid body or query parameter (schema validation) |
QUERY_FAILED | 400 | The target database rejected a statement |
UNAUTHORIZED | 401 | No valid session cookie |
FORBIDDEN | 403 | Signed in, but not allowed to do this |
NOT_FOUND | 404 | No such resource |
CONFLICT | 409 | The request contradicts current state, such as deleting a connection a bridge still uses |
RATE_LIMITED | 429 | Too many login or setup attempts; wait and retry |
INTERNAL | 500 | Unexpected error |
UNSUPPORTED | 501 | The engine cannot do what was asked |
CONNECTION_FAILED | 502 | The target database could not be reached |
Authentication
Syncle has a single admin account, created on first run with the setup token (the quickstart walks through that). Signing in sets db_session, a signed httpOnly cookie with SameSite=Lax, valid for the sessionTtlMinutes setting — one week by default. Keep it in a cookie jar and send it back on every call:
# sign in once, keeping the cookie
curl -c cookies.txt -H 'Content-Type: application/json' \
-d '{"username":"admin","password":"your-password"}' \
http://localhost:3002/api/auth/login
# every later call sends it back
curl -b cookies.txt http://localhost:3002/api/bridgesRepeated failed logins lock the account out per IP and username and answer 429 with code RATE_LIMITED; setup attempts are limited per IP. Changing the password invalidates every outstanding session at once — the response re-issues the cookie for the session that made the change.
| Endpoint | Body | Purpose |
|---|---|---|
GET /api/auth/status | — | Returns { needsSetup, authenticated, user }; public, so a client can decide which screen to show |
POST /api/auth/setup | { username, password, setupToken } | Create the admin account on first run and sign in |
POST /api/auth/login | { username, password } | Sign in; sets the session cookie |
POST /api/auth/logout | — | Clear the cookie |
GET /api/auth/me | — | The signed-in user |
POST /api/auth/change-password | { currentPassword, newPassword } | Change the password; invalidates all other sessions |
Workspaces
Workspaces are the top-level container for connections and bridges. A default workspace always exists, so you only meet these routes once you create a second one.
| Endpoint | Purpose |
|---|---|
GET /api/workspaces | List workspaces |
POST /api/workspaces | Create one — { name, color? } |
GET /api/workspaces/:id | Fetch one |
PUT /api/workspaces/:id | Update name or color |
DELETE /api/workspaces/:id | Tears down every bridge inside — CDC slots dropped, watchers stopped, in-flight jobs canceled — then deletes the workspace |
Connections
A connection is a saved database config. Passwords, SSH secrets and connection strings are encrypted at rest and redacted in every response. Routes that touch data accept a ?database= query parameter to address one database on the server; leave it off to use the connection's default.
| Endpoint | Purpose |
|---|---|
GET /api/connections?workspaceId= | List connections |
POST /api/connections | Create one |
POST /api/connections/test | Test an unsaved config without storing it |
GET /api/connections/:id | Fetch one |
PUT /api/connections/:id | Update; any pooled connection is evicted |
DELETE /api/connections/:id | Delete — answers 409 CONFLICT while any bridge still uses it as source or destination |
POST /api/connections/:id/test | Test a saved connection |
Data operations
These are the routes behind the database workbench. What each engine supports varies — the driver's capability flags say which of them apply.
| Endpoint | Purpose |
|---|---|
GET /api/connections/:id/databases | List databases on the server |
GET /api/connections/:id/schema?database= | Introspect tables, columns, keys and indexes |
POST /api/connections/:id/browse?database= | Paged reads with filters and sort; limit 1–1000, default 100 |
POST /api/connections/:id/query?database= | Ad-hoc query — { statement, params } |
POST /api/connections/:id/rows | Insert a row |
PATCH /api/connections/:id/rows | Update a row, identified by its primary key values |
DELETE /api/connections/:id/rows | Delete a row |
DDL, backup and restore
| Endpoint | Purpose |
|---|---|
POST /api/connections/:id/ddl/database | Create a database — { name } |
POST /api/connections/:id/ddl/drop-database | Drop a database (pooled connections to it close first) |
POST /api/connections/:id/ddl/table?database= | Create a table from a column-definition list |
POST /api/connections/:id/ddl/drop-table?database= | Drop a table — { table, schema? } |
POST /api/connections/:id/ddl/truncate-table?database= | Truncate a table |
POST /api/connections/:id/backup?database= | Dump a database; returns { filename, format, content } |
POST /api/connections/:id/restore?database= | Restore from { content, format } |
Backups come in two formats: json, a portable dump any engine can read back, and sql, a DDL-plus-INSERT script for relational engines only — MongoDB and Redis support just json.
Drivers and settings
GET /api/drivers lists the five supported engines with their labels, default ports, capability flags and the fields their connection forms need — the interface builds its connection dialog from this list, and a script can do the same.
GET /api/settings returns the resolved app settings (stored overrides merged over defaults); PUT /api/settings applies a partial update. The configuration page documents each setting and its default.
Bridges
A bridge is the saved sync path, a job is one execution of it, and a delivery is one row or batch within a job — how bridges work covers the model. The routes below manage all three.
| Endpoint | Purpose |
|---|---|
GET /api/bridges?workspaceId= | List bridges |
GET /api/bridges/statuses?workspaceId= | Latest job status per bridge, in one call |
POST /api/bridges | Create a bridge; a draft job is prepared so the timeline shows the planned deliveries right away |
GET /api/bridges/:id | Fetch one |
PUT /api/bridges/:id | Update; a live watch or CDC listener is stopped first and restarted on the new config |
DELETE /api/bridges/:id | Full lifecycle teardown, then delete |
Jobs and deliveries
| Endpoint | Purpose |
|---|---|
POST /api/bridges/:id/preview | Render what would be delivered without delivering — body { sampleRow?, limit }, limit 1–10, default 3 |
POST /api/bridges/:id/jobs | Start a job; the body may carry resumeJobId, jobId (start a prepared draft) or retryFailedOf |
GET /api/bridges/:id/jobs | List jobs |
GET /api/bridges/:id/jobs/:jobId | Job detail — status, counts, cursor, error |
POST /api/bridges/:id/jobs/:jobId/retry-failed | Re-queue the same job to re-send only its failed deliveries |
POST /api/bridges/:id/jobs/:jobId/cancel | Cancel; for a watch or CDC job this stops the listener and the job pauses, keeping its cursor |
GET /api/bridges/:id/jobs/:jobId/deliveries | Delivery list; filters status= (one of success, failed, skipped), from, to, offset, limit |
POST /api/bridges/:id/jobs/:jobId/skip | Skip queued deliveries by { sequences }; returns { skipped } |
The numeric delivery-list parameters must be non-negative integers — anything else is a 400, not an empty result. Skipping only affects deliveries that are still queued.
Live listening
| Endpoint | Purpose |
|---|---|
POST /api/bridges/cdc/readiness | Probe whether a source can do CDC — { connectionId, database?, schema?, table }; see CDC setup for what readiness means per engine |
POST /api/bridges/:id/watch/start | Start live listening; routed to CDC or polling watch by the bridge's trigger |
POST /api/bridges/:id/watch/stop | Stop listening and return the finalized job |
Limits
- JSON request bodies cap at 50 MB — sized so backup and restore payloads, which carry whole dumps, fit.
- Ad-hoc query results cap at the
maxQueryRowssetting, default 5000 rows; the configuration page covers raising it globally or per connection. - Browse pages return at most 1000 rows per request (default 100).
- A single skip call accepts at most 10,000 sequence numbers.
