REST API

Quickstart

Get an API key, point at your instance, make your first request, and learn the conventions you'll see across all 400+ endpoints.

Prerequisites

Step 1: Authentication

Two authentication modes are supported. Pick whichever fits your use case:

Option A: API key (recommended for service accounts)

  1. Sign in to your Etlworks instance.
  2. Open Settings → Users, select the user, click Edit.
  3. Generate an API key under API Key. Optionally set an expiration.
  4. Copy the key now, you won't see it again after closing the dialog.
Treat keys like passwords

API keys inherit the generating user's role and tenant scope. Anyone with the key can do whatever that user can do. Store keys in a secrets manager: never commit them to git or paste into chat.

Send the key on every request via the Authorization header:

HTTP
Authorization: Bearer YOUR_API_KEY

Option B: Username/password → JWT

For interactive apps, exchange username + password for a short-lived JWT, then send the JWT on subsequent calls. Same Authorization: Bearer … header.

Shell
curl -s -X POST https://app.etlworks.com/rest/v1/token-auth/issue \
  -H "Content-Type: application/json" \
  -d '{"login":"alice@example.com","password":"…"}'
# → { "token": "eyJhbGciOi…" }

JWTs last 8 hours by default (on-premise, integrator.jwt.exp.period changes it). Refresh one that has not expired with POST /v1/token-auth/refresh and body {"token": "…"}, or re-authenticate. If the user has two-factor authentication on, the first call returns tfaToken instead of token: repeat it with tfaToken and the one-time code.

Step 2: Base URL

HTTP
https://your-instance.etlworks.com/rest

Replace your-instance.etlworks.com with your deployment hostname. Etlworks Cloud customers use app.etlworks.com. For on-premise installations use the host you configured.

All endpoints in this reference are relative to /rest. So /v1/flows in the docs means https://your-instance.etlworks.com/rest/v1/flows on the wire.

Step 3: Your first request

List your flows. This is the cheapest call you can make: pure read, returns fast, exercises auth.

Shell
curl -s https://app.etlworks.com/rest/v1/flows \
  -H "Authorization: Bearer YOUR_API_KEY"

You should see a JSON array of Flow objects (timestamps are unix milliseconds):

JSON
[
  {
    "id": 12345,
    "name": "Stripe to Snowflake — daily",
    "description": "Daily incremental sync of charges into the warehouse",
    "flowType": "flow.db.db",
    "tags": ["production","stripe"],
    "enabledSchedules": 1,
    "disabledSchedules": 0,
    "createdBy": "alice@example.com",
    "created": 1715366462000,
    "modifiedBy": "alice@example.com",
    "modified": 1715369462000
  }
]
401 Unauthorized?

Check the Authorization header is Bearer <key> with a single space and no quotes. The key string itself has no Bearer prefix.

404 Not Found?

Confirm your base URL is correct and includes /rest. The full path for this call is /rest/v1/flows, not /v1/flows or /api/v1/flows.

Step 4: Make a write call

Run a flow. Substitute a real flow ID from Step 3. The request body is a flat map of {string: string} parameters, not wrapped in {"params": …}. Send {} if your flow takes no parameters.

Shell
curl -s -X POST https://app.etlworks.com/rest/v1/flows/12345/run \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"runDate":"2026-05-09"}'

The response is a FlowExecutionResponse with the auditId you'll use to poll progress:

JSON
{
  "flowId": 12345,
  "auditId": 8721934,
  "status": 0
}

The numeric status is the start outcome (0 = started, 1 = already running, 2 = canceled, etc.), not the run's progress. To track progress, poll GET /v1/executions/{flowId}?auditId={auditId}; that returns a FlowAuditRecord with a string status field whose values follow the FlowStatus enum: queued, running, success, warning, error, canceled. Loop until you see one of the terminal four. The run-and-wait example on the examples page shows the full pattern.


Content types

The API speaks JSON throughout, with two exceptions: file uploads (multipart/form-data) and a handful of legacy export endpoints that return text/html for browser-rendered diagrams. Defaults:

Lists and pagination

List endpoints are not paginated. Each one returns everything in scope in a single response, so GET /v1/flows returns every flow you can see.

The exception is execution history. GET /v1/executions/{flowId}/history caps its result and reports how many records it left out in the X-Truncated-Count response header. To page back further, pass the oldest auditId you received as lastAuditId, or narrow the window with fromDate and toDate.

Filtering and search

There is no generic filter syntax. Filters are specific to each endpoint:

To findCall
Anything by nameGET /v1/search?query=stripe searches flows, connections, formats, schedules, webhooks, agents, macros and templates.
Flows across the tenantGET /v1/flows?scope=all
Executions in a window or with a statusGET /v1/executions/{flowId}/history?fromDate=2026-09-01&status=error
The schedules that run a flowGET /v1/schedules/flows/{flowId}
Tags in useGET /v1/tags

To filter by tag or type, list the objects and filter the JSON on your side: each object carries tags and its type key. Every endpoint's parameters are in the API reference and the OpenAPI spec.

Error handling

HTTPMeaningAction
200Success, including creates, which return the saved object with its new id, and most deletes.Parse normally.
204No content. Returned only when deleting a webhook.Empty body is expected.
400Bad request: missing field, malformed JSON, or invalid values.Read the message and fix the request.
401Missing, invalid or expired token. Empty body.Re-authenticate or rotate the API key.
403The token's role does not allow this call. Usually an empty body.Use a key for a user with the right role. API User keys cannot manage flows, schedules or connections.
404Not found.Check the id exists and the user can see it.
409A delete was refused because something still references the object.The body lists the references. Remove them first, or for flows pass schedules=true.
415Wrong Content-Type.JSON endpoints want application/json.
500Server error.Retry with backoff; if it persists, contact support with the time and endpoint.

Error bodies are not a single envelope. A 400 carries the message either as plain text or as JSON:

JSON
{ "error": "Field 'name' is required" }

401 and 403 have an empty body. A 500 carries the message as plain text, or nothing for database errors. Read the status code first and treat the body as a human-readable hint.

Rate limits

The platform REST API has no application-level rate limit. Be reasonable with bulk work anyway: an export, an import or a run of every flow in a tenant is real load on the instance that runs your pipelines.

HTTP listener endpoints are different. They can be throttled by the listeners.throttle.enabled setting, which is off by default and allows 100 requests per minute when on. Requests over the limit are queued by listener priority or rejected with 429 Too Many Requests. There is no Retry-After header, so back off on your side.


You're set

You've got auth, base URL, your first read and write, and the conventions you'll see everywhere. From here: