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
- An Etlworks account (cloud, hybrid, or on-premise). Sign up free if you don't have one.
- A user with permission to generate API keys (admin, or a role with the
API accesscapability). - Either
curl, Python 3.7+, or any HTTP client you prefer.
Step 1: Authentication
Two authentication modes are supported. Pick whichever fits your use case:
Option A: API key (recommended for service accounts)
- Sign in to your Etlworks instance.
- Open Settings → Users, select the user, click Edit.
- Generate an API key under API Key. Optionally set an expiration.
- Copy the key now, you won't see it again after closing the dialog.
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:
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.
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
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.
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):
[
{
"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
}
]
Check the Authorization header is Bearer <key> with a single space and no quotes. The key string itself has no Bearer prefix.
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.
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:
{
"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:
- Request body:
application/jsonon all POST/PUT calls (Content-Type required). - Response body:
application/json, except endpoints explicitly marked otherwise. - Character encoding: UTF-8 throughout.
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 find | Call |
|---|---|
| Anything by name | GET /v1/search?query=stripe searches flows, connections, formats, schedules, webhooks, agents, macros and templates. |
| Flows across the tenant | GET /v1/flows?scope=all |
| Executions in a window or with a status | GET /v1/executions/{flowId}/history?fromDate=2026-09-01&status=error |
| The schedules that run a flow | GET /v1/schedules/flows/{flowId} |
| Tags in use | GET /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
| HTTP | Meaning | Action |
|---|---|---|
200 | Success, including creates, which return the saved object with its new id, and most deletes. | Parse normally. |
204 | No content. Returned only when deleting a webhook. | Empty body is expected. |
400 | Bad request: missing field, malformed JSON, or invalid values. | Read the message and fix the request. |
401 | Missing, invalid or expired token. Empty body. | Re-authenticate or rotate the API key. |
403 | The 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. |
404 | Not found. | Check the id exists and the user can see it. |
409 | A delete was refused because something still references the object. | The body lists the references. Remove them first, or for flows pass schedules=true. |
415 | Wrong Content-Type. | JSON endpoints want application/json. |
500 | Server 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:
{ "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've got auth, base URL, your first read and write, and the conventions you'll see everywhere. From here: