User-defined APIs
Expose any flow as a custom REST API endpoint, or have an external system trigger a flow by POSTing to one. Configure once, your endpoint lives at /rest/v1/httplistener/<your-pattern>, runs under the calling user's permissions, and shows up in the audit log alongside everything else.
The endpoints in the REST API reference are built-in: /v1/flows, /v1/connections, etc., for managing the platform. User-defined APIs are endpoints you create by attaching a flow to an HTTP Listener. They're how you expose your own data and integrations as REST.
Overview
The core building block is an HTTP Listener: a connection that listens on a configurable path and method. Depending on which side of the source-to-destination flow it sits on, the listener either pushes the request payload into the flow or returns the flow's output as the HTTP response:
| Pattern | Listener role | Use case |
|---|---|---|
| PULL | Destination (TO) | Expose a database, file, or third-party API as a JSON/XML/CSV endpoint. |
| PUSH | Source (FROM) | External system POSTs a payload → Etlworks runs a flow that processes it. |
| UPLOAD | Source (FROM) | POST a file body → Etlworks copies the bytes into file storage (S3, FTP, server, …). |
| CRUD | Source or destination per method | One resource exposed across POST, PUT, DELETE, GET. |
What a request looks like
| Aspect | Spec |
|---|---|
| Path | /rest/v1/httplistener/<url-pattern> |
| Methods | GET, POST, PUT, DELETE or OPTIONS: one per listener, the one you configured |
| Auth header | Authorization: Bearer <JWT or API key> · or Authorization: Basic <base64(user:pass)>, in the header or as an Authorization query parameter |
| Request body | Any supported format: JSON, XML, CSV, fixed-length, HL7. Empty body allowed (see Empty payload). |
| Content-Type | Not required: the listener parses the body by its Format. A application/x-www-form-urlencoded body is read as form fields. |
| Response | The flow's output (GET) · a receipt (POST/PUT) · an empty body with 204 (DELETE/OPTIONS, by default) · anything the flow writes (see Custom responses). The content type follows the listener's Format: JSON → application/json, XML → application/xml, anything else text/plain, unless the flow sets Content-Type. |
| Status codes | 200 success, 204 for DELETE/OPTIONS; errors from 400 to 503: see Response codes |
Receipt for POST and PUT
Unless the flow returns its own body, a POST or PUT gets a JSON receipt. Its shape depends on the listener's POST/PUT processing setting.
Async (the default): the receipt comes back as soon as the payload is stored, before the flow runs, so it has no auditId. status is received, or queued when throttling queued the request.
{
"flowId": 3219,
"messageUuid": "caaa8347-063c-449b-a29b-12e68799c986",
"status": "received"
}
Sync: the caller waits for the flow. The receipt carries the execution’s auditId and its outcome, success or warning; a failed run returns an error status instead (see Response codes).
{
"flowId": 3219,
"messageUuid": "caaa8347-063c-449b-a29b-12e68799c986",
"auditId": 106358,
"status": "success"
}
Endpoint URL
Every user-defined API lives at:
https://<your-instance>.etlworks.com/rest/v1/httplistener/<your-url-pattern>
For Etlworks Cloud customers, the host is app.etlworks.com. For on-prem, it's your configured hostname.
URL pattern
Set the URL Pattern field when creating the HTTP Listener. The pattern becomes the path your endpoint responds at:
| URL Pattern | Resulting endpoint |
|---|---|
patients | https://app.etlworks.com/rest/v1/httplistener/patients |
hl7/patient | https://app.etlworks.com/rest/v1/httplistener/hl7/patient |
patients/{patientid} | https://app.etlworks.com/rest/v1/httplistener/patients/123: {patientid} binds to the path segment |
Only one HTTP Listener can claim a given pattern at a time. /patients and /patient are different patterns and can coexist; two listeners both claiming /patients cannot.
Path and query parameters become flow variables
Tokens like {patientid} in the URL pattern are extracted from the request path, URL-decoded, and exposed to the flow as variables: usable in source SQL, the FROM/TO fields, and JavaScript / Python. Query parameters are passed the same way (all of them except Authorization), so ?status=active becomes {status}. Do not put query parameters in the URL pattern itself.
patients/{patientid}
select * from patient where patientid = {patientid}
You can also surface request headers as variables: see Request & response headers.
Authentication: JWT (recommended)
Auth Type JWT is the default. Auth Location is either Header (the default, recommended) or Query Parameter. Despite the name, this auth type accepts both a JWT and an API key as the bearer token.
Three steps to call the endpoint
- Create an API user with a role that can execute flows. (The API User role can call listener endpoints and the Messages and CLI endpoints of the platform API, but not flows, schedules or connections.)
- Exchange username + password for an access token via the auth endpoint:
Or skip this step entirely: generate an API key, with or without an expiry date (Settings → Users → Edit user → API Key), and send it exactly where a JWT would go.Shell
curl -s -X POST https://app.etlworks.com/rest/v1/token-auth/issue \ -H "Content-Type: application/json" \ -d '{"login":"api-user","password":"…"}' # → { "token": "eyJ0eXAiOiJKV1Q…" } - Send the token on every call to your endpoint:
Auth location Where the token goes Header Authorization: Bearer eyJ0eXAi…Query Parameter ?Authorization=Bearer%20eyJ0eXAi…
Tokens minted via /v1/token-auth/issue last 8 hours by default (on-premise, integrator.jwt.exp.period changes it). Refresh one before it expires with POST /v1/token-auth/refresh, or use an API key for service accounts.
Authentication: Basic
Less secure than JWT: use only when the calling system can't manage tokens (e.g. a third-party webhook that only sends a static Authorization header).
Set Auth Type to Basic on the listener. Auth Location is Header or Query Parameter; with Query Parameter, the credentials can also be embedded in the URL path (see next section). Users with two-factor authentication turned on cannot authenticate with Basic: use an API user without 2FA, or JWT.
- Base64-encode
user:password:Shellprintf 'admin:admin' | base64 # → YWRtaW46YWRtaW4= - Send on every call:
Auth location Where it goes Header Authorization: Basic YWRtaW46YWRtaW4=Query Parameter ?Authorization=Basic%20YWRtaW46YWRtaW4=
Path-based auth (for restrictive webhooks)
Some third-party webhook systems (eBay, certain SaaS platforms) only let you configure a static URL, no header, no query string. The workaround: embed a doubly-base64-encoded Basic auth string as the last path segment.
- Base64-encode
user:password:admin:admin→YWRtaW46YWRtaW4= - Build the standard Basic auth string:
Basic YWRtaW46YWRtaW4= - Base64-encode that:
QmFzaWMgWVdSdGFXNDZZV1J0YVc0PQ== - On the listener, set Auth Type to
Basic, Auth Location toQuery Parameter, and end the URL Pattern with a token:some/path/{auth}. (A URL pattern can only contain letters, digits,. - _ /, so the encoded string itself, with its=padding, cannot be part of the pattern.) - Point the webhook at the URL with the encoded string as the last segment:
…/rest/v1/httplistener/some/path/QmFzaWMgWVdSdGFXNDZZV1J0YVc0PQ==
When a request has no Authorization query parameter, the listener base64-decodes the last path segment and authenticates with it if it starts with Basic. The encoded string must not contain /; if it does, change the password. Changes to a listener can take up to a minute to take effect.
PULL: expose a data source as an API endpoint
Tableau wants the latest customer list, in JSON, behind an authenticated URL. Wire up a flow whose source is your warehouse and whose destination is the listener: the listener serves the flow's output as the response.
Setup
- Create an HTTP Listener with Method =
GETand a URL pattern. - Create a Format for the response: JSON, JSON Dataset, XML, XML Dataset, CSV, or Fixed-Length Text.
- Create an "any-to-web-service" flow where the source is your data (database, file, API) and the destination is the listener + format from steps 1 & 2.
- Configure the source query, mapping, and any flow parameters needed. Use
{patientid}-style tokens to bind path parameters into the SQL. - Schedule the flow as event-driven. Etlworks executes it on every inbound
GET.
Calling it
curl -s "https://app.etlworks.com/rest/v1/httplistener/patients/123" \
-H "Authorization: Bearer eyJ0eXAi…"
# → { "id": 123, "name": "…", … }
Test it from inside Etlworks Explorer too: create an Etlworks API Connection pointing at the endpoint and browse the response there.
PUSH: trigger a flow when a payload arrives
A SaaS application updates a record. Its webhook fires and POSTs the updated record's JSON to your endpoint. Your flow runs: transforms the payload, writes it to the warehouse, sends an email.
Setup
- Create an API user with a role that can run flows.
- Create an HTTP Listener with Method =
POST(orPUT). - Create a Format matching the inbound payload: JSON, XML, CSV, fixed-length, HL7 v2.x, FHIR.
- Build a flow whose source is a web service. The source connection (FROM) is the listener.
- Add mapping + parameters as you would normally.
- Schedule the flow as event-driven. It runs every time a payload arrives.
A flow can have at most one listener as its source connection. If you need a second inbound endpoint for the same processing logic, create a second flow that calls the first as a nested flow.
Calling it
curl -s -X POST "https://app.etlworks.com/rest/v1/httplistener/hl7/patient" \
-H "Authorization: Bearer eyJ0eXAi…" \
-H "Content-Type: application/json" \
-d '{"id":123,"name":"…"}'
# → { "auditId": 106358, "messageUuid": "…", "flowId": 3219, "status": "success" }
UPLOAD: ingest files via HTTP
An HTTP Listener can be the source of a Copy Files flow: turning a POST endpoint into a file dropzone that lands bytes in any supported file storage (server, FTP/FTPS/SFTP, S3, etc.).
Setup
- Create an HTTP Listener with Method =
POSTand an Auth Type. URL Pattern uses a path token for the filename:URL Patternfile/{filename} - Create a Copy Files flow.
- Set the FROM connection to the listener.
- Set the TO connection to any file connection.
- Set the destination filename to either a literal value (
test.json) or the path token ({filename}) so the caller controls it. - Schedule the flow as event-driven.
Calling it
curl -s -X POST "https://app.etlworks.com/rest/v1/httplistener/file/test2.json" \
-u "api-user:…" \
--data-binary @local-file.json
# The body of local-file.json lands at the configured destination as test2.json.
CRUD: multiple methods on one resource
Build a full create / read / update / delete API by configuring listeners with the appropriate HTTP method and wiring each to a flow. Path tokens parameterize destination queries.
| Method | Wiring | Execution mode |
|---|---|---|
GET | Listener as destination: same as PULL. | Synchronous: caller waits for the flow. |
POST | Listener as source. Destination is the resource you're creating into. Tokenize the destination query with path / payload variables. | Set by POST/PUT processing: Async (default) returns a receipt before the flow runs; Sync waits for it. |
PUT | Same as POST; pick PUT when semantics matter. | Same as POST: Async by default, Sync if set. |
DELETE | Same shape as GET, but configure the listener with DELETE. Returns an empty body with the status set by Successful DELETE or OPTIONS response code: 204 (default) or 200. | Synchronous. |
OPTIONS | Same as DELETE: an empty body with the configured status and an Allow: GET, POST, PUT, DELETE, OPTIONS header. Useful for clients that probe an endpoint first. | Synchronous. |
Example: tokenized destination query
update patient
set name = {name}
where patientid = {patientid}
With URL Pattern patients/{patientid} and a JSON payload {"name":"Jane"}, calling PUT /rest/v1/httplistener/patients/123 with body {"name":"Jane"} resolves to update patient set name='Jane' where patientid=123.
Custom responses
By default, POST and PUT endpoints return the receipt. Setting a POST/PUT Response File Name makes the listener synchronous even if POST/PUT processing says Async, since it has to wait for the file. Two ways to use it:
Option A: return the destination's response
Useful when the flow forwards the payload to another HTTP API and you want to relay that API's reply back to the caller.
- On the listener, set POST/PUT Processing =
Syncand POST/PUT Response File Name ={app.data}/response.json. - On the destination HTTP connection, set Response File Name to the same path.
- Build the flow with the listener as source and that HTTP connection as destination.
- Schedule event-driven.
The destination's response gets written to response.json and returned to the caller. The file is auto-deleted after the response goes out.
Option B: user-defined response file
Generate a response from inside the flow itself, without delegating to an upstream API.
- Listener: POST/PUT Processing =
Sync, POST/PUT Response File Name ={app.data}/response.json. - Build a primary flow that processes the inbound payload.
- Build a second flow that writes the response file (same path as in step 1).
- Combine them into a nested flow so the response file always exists by the time the listener replies.
- Schedule the nested flow event-driven.
The response file is auto-deleted after the listener returns it.
Choose the status code from the flow
Set HTTP Code Variable on the listener to the name of a global variable, and set that variable in the flow; the listener returns its value as the status. Codes of 400 and above are returned as errors. If the field is empty or the variable is not set, the default code applies.
com.toolsverse.config.SystemConfig.instance().getProperties().put('patient_status', '404');
For GET, Empty GET response code decides what an empty result (nothing, {} or []) returns: 200 (default, the body as is), or 204, 400 or 404 with an empty body.
Request & response headers
Read inbound headers
Enable Pass request headers to the flow as parameters on the listener (off by default). Each header except Authorization becomes a flow variable. Variable lookups are case-insensitive to the uppercase form, so Validation-Token can be read as VALIDATION-TOKEN.
var token = scenario.getVariable('VALIDATION-TOKEN').getValue();
Set outbound headers
From any place a flow runs JavaScript or Python:
etlConfig.addResponseHeader('header-name', headerValue);
Setting Content-Type this way overrides the content type the listener would otherwise derive from its Format.
Real-world example: webhook subscription handshake
RingCentral's webhooks require the endpoint to echo a Validation-Token request header back in the response to complete the subscription. The flow:
var token = scenario.getVariable('VALIDATION-TOKEN');
etlConfig.addResponseHeader('Validation-Token', token != null ? token.getValue() : '');
value = true;
Empty payload
Some upstream systems POST with no body, especially webhook handshakes. Without a default, the parser raises an exception. Fix: set Default Payload on the listener to a valid document in the chosen format. The flow runs against the default whenever a POST or PUT body is empty.
Request preprocessor
For POST and PUT, Request Preprocessor script runs JavaScript against the raw request body before anything else happens: before the payload is stored and before the flow starts. The script sees the request as request:
request.getInput()is the raw body;request.setInput(newBody)replaces what the flow receives.request.setSuccess(false)answers the caller without running the flow: the value of the script’s last expression is sent back with200. If that value is empty, the caller gets500and a receipt with"status": "error".
var body = JSON.parse(request.getInput());
if (body.type == 'ping') {
request.setSuccess(false); // reply now, skip the flow
JSON.stringify({ pong: true }); // last expression = the reply
} else {
request.setInput(JSON.stringify(body.data)); // pass only the data element on
}
Binary payloads
The body is read as UTF-8 text by default, and characters that are not valid UTF-8 fail the request with 400. Two settings change that:
- Non strict UTF-8 payload encoding accepts payloads with invalid characters instead of rejecting them.
- Preserve raw request body reads POST/PUT bodies as bytes and passes them to the flow Base64-encoded in
http_request_body_base64, withhttp_request_body_is_base64,http_request_content_length,http_request_content_typeandhttp_request_character_encodingalongside. Use it for files, images and other binary protocols.
Response codes
| Code | When |
|---|---|
200 | Success: GET with a body, POST/PUT receipt or response file. |
204 | Successful DELETE or OPTIONS (default; Successful DELETE or OPTIONS response code can make it 200). A GET with an empty result if Empty GET response code is 204. |
400 | The body contains characters that are not valid UTF-8 (see Binary payloads). Also returned for an empty GET result if configured. |
401 | Missing or invalid credentials, or Basic auth for a user with two-factor authentication. |
403 | Authenticated, but the user belongs to a different tenant than the listener. |
404 | No listener matches the path and method. Also returned for an empty GET result if configured. |
429 | Throttling is on, the per-minute limit is reached, and the request was not queued (see Queueing and throttling). Only async POST/PUT requests are throttled. No Retry-After header. |
500 | The flow failed or the payload could not be read. Check the execution in the audit trail; the message is stored under the request’s messageUuid. |
503 | The instance is suspended (for example during an upgrade) and not accepting new requests. |
| Any | The value of the global variable named in HTTP Code Variable, when the flow sets one (see Custom responses). |
Test from inside Etlworks
You don't need an external system to exercise an event-driven flow. Build a "sender" flow that calls your listener.
- Prepare test data. A source connection (file, cloud storage, database) plus a Format matching what real callers will send.
- Configure the destination. Create an Etlworks API Connection with:
- Endpoint path:
/rest/v1/httplistener/<your-pattern> - Method: matches your listener (
POST,PUT,GET,DELETE) - User / password: the API user configured for the listener
- Endpoint path:
- Build the sender flow. Source = test data + format. Destination = the API connection from step 2. Format on both sides matches.
- Run the sender. It POSTs (or GETs / PUTs / DELETEs) to your listener, triggering the event-driven flow exactly as a real caller would.
The sender flow is reusable: keep it around for regression testing or wire it into a CI flow.
View inbound payloads
POST and PUT payloads are kept in the platform’s messages store, queryable by id, by listener, by status and by substring. Async requests are always stored; synchronous ones only when Keep message is on in the listener’s schedule. GET, DELETE and OPTIONS requests have no payload to store. Use the Messages app in the UI, or the CLI:
// Filter recent messages
list-messages-filtered message=Notification;
// Pull one message body
get-message 24a6e77b-c05f-45ed-b1e8-908ce361bf31;
Or via REST: GET /v1/messages: see Messages in the REST API reference.
Queueing and throttling
On busy instances, async POST and PUT requests to listeners can be rate-limited. Throttling is a global setting, off by default; when on, it allows 100 requests per minute unless configured otherwise (on-premise: listeners.throttle.enabled and listeners.throttle.throughput). Requests over the limit are either queued, and answered with a receipt whose status is queued, or rejected with 429. A listener’s Queue priority (1 high to 10 low) orders its queued requests against other listeners’. GET, DELETE, OPTIONS and synchronous POST/PUT requests are never throttled.
AS2 listeners
An AS2 listener is an HTTP listener for inbound AS2 messages, at the same /rest/v1/httplistener/<url-pattern> address. It accepts POST only, keeps the raw body, decrypts and verifies the message with the certificates you configure (local decryption certificate, partner signing certificate, AS2-To and optional AS2-From), and returns a synchronous MDN, signed or unsigned. Authentication is optional (None or Basic), because AS2 exchanges usually rely on TLS and certificates. The decrypted payload goes to the flow like any other listener payload.
The NBCUniversal and Transport for NSW case studies are both built on this exact pattern: user-defined APIs in front of multi-source flows. Drop us a note at product@etlworks.com if you hit anything that's not in this guide.