Skip to content
Documentation for: Echo
HUuptools.io
Markdown

Quickstart

https://echo.uptools.io/public/v1

The API page under Integrations in the app shows the same address next to Base URL. The examples below use two shell variables:

Terminal window
export ECHO_API=https://echo.uptools.io/public/v1
export ECHO_TOKEN=upm_... # your API token, see Authentication

Create a token under Integrations, API, Generate new token (Authentication), then count your active subscribers:

Terminal window
curl -H "Authorization: Bearer $ECHO_TOKEN" "$ECHO_API/subscribers/count"
HTTP/1.1 200 OK
Content-Type: application/json
X-Ratelimit-Limit: 120
X-Ratelimit-Remaining: 119
{"data":{"total":4712}}

Add a subscriber (a Full access token is needed):

Terminal window
curl -X POST "$ECHO_API/subscribers" \
-H "Authorization: Bearer $ECHO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","fields":{"name":"Lilla","city":"Szeged"}}'

The answer is 201 Created with the new subscriber, or 200 OK if the address was already on the list. The details are in Subscribers.

  • Every request needs Authorization: Bearer <token>.
  • Request bodies are JSON only: send Content-Type: application/json. Anything else is 415 unsupported_media_type.
  • A body must be one JSON object. Broken JSON, or anything that isn’t one object, is 400 invalid_json. Unknown keys are refused with 422 validation (the key is named in fields), so a typo never goes unnoticed.
  • A body can be at most 1 MB; an import body at most 10 MB.
  • Accept-Language: en or hu chooses the language of error messages. Data (field names, group names) is never translated.
  • Query parameters with brackets, such as filter[status], need curl -g (or URL encoding: filter%5Bstatus%5D), otherwise curl reads the brackets as a pattern.
  • IDs are UUIDs. Every time in an API answer is UTC, as an RFC 3339 timestamp ending in Z (for example 2026-10-22T08:52:08.527488Z). Webhook payloads can also carry times with an offset (Webhooks), so parse times as RFC 3339.
  • HTML in JSON strings is escaped as \u003c and \u003e. Any JSON parser turns these back into < and >.

A single object comes in data:

{"data":{"id":"257e83a2-d55e-4a8c-b9a4-b3e939c151cc","name":"Webshop vásárlók","created_at":"2026-10-22T08:52:08.269421Z","subscribers":0,"total":0,"open_rate":0,"click_rate":0}}

A list comes in data with paging details in meta:

{"data":[...],"meta":{"next_cursor":"bzoz","prev_cursor":null,"limit":3,"total":7}}

Successful deletes and actions without content answer 204 No Content with an empty body. Errors have their own format, see Errors and limits.

Lists take limit (1 to 100, default 25) and return cursors in meta:

Key Meaning
next_cursor Pass it as cursor to get the next page. null on the last page.
prev_cursor Pass it as cursor to get the previous page. null on the first page.
limit The page size used.
total The number of matching items, when counting was cheap. It can be missing.

Cursors are opaque strings: pass them back as they are, always in cursor, whichever way you page. Two lists page forward only: a subscriber’s activity and a workflow’s runs. Their prev_cursor is always null.

The subscriber lists still accept the older before parameter for a prev_cursor, but it is deprecated: use cursor.

Example, the first page of groups and then the second:

Terminal window
curl -g -H "Authorization: Bearer $ECHO_TOKEN" "$ECHO_API/groups?limit=3"
curl -g -H "Authorization: Bearer $ECHO_TOKEN" "$ECHO_API/groups?limit=3&cursor=bzoz"
{"data":[{"id":"26e38149-d391-4215-86e4-92bb883e5910","name":"Új regisztrálók","created_at":"2026-09-20T14:45:00Z","subscribers":427,"total":451,"open_rate":37.55,"click_rate":7.72,"timed_out":false},{"id":"000d6529-0078-40d8-9c29-05cd541c4c2d","name":"VIP vásárlók","created_at":"2026-04-17T12:00:00Z","subscribers":466,"total":495,"open_rate":34.64,"click_rate":7.47,"timed_out":false},{"id":"19e19d19-d3df-4763-965c-d2f4d974e40a","name":"Webinar 2026","created_at":"2026-07-28T09:00:00Z","subscribers":413,"total":438,"open_rate":34.68,"click_rate":8.32,"timed_out":false}],"meta":{"next_cursor":"bzo2","prev_cursor":"bzow","limit":3,"total":7}}

Subscriber list cursors are encrypted and can expire. An expired cursor answers 422 with fields.cursor: start again from the first page. Don’t store cursors for long.

A limit outside 1 to 100 or a broken cursor is 422 validation:

{"error":{"code":"validation","message":"Please check the highlighted fields.","fields":{"limit":"Invalid value."}}}

The API has no idempotency key header. Plan your retries per endpoint:

Request Safe to repeat?
GET requests Yes.
POST /subscribers Yes: it adds or updates by email address, so a repeat updates the same subscriber.
PUT changes (subscriber, group or segment name, field name, webhook) Yes: the same body gives the same result.
POST /subscribers/{id}/groups/{group_id} Yes: an existing membership stays as it is.
DELETE The first one answers 204, a repeat 404 not_found. Treat 404 after a timeout as done.
POST /groups/{id}/import-subscribers A repeat starts a second import. The import itself updates existing addresses, so the subscribers aren’t duplicated, but check the first import’s status before you resend.
POST /campaigns, POST /webhooks, POST /groups, POST /fields No: a repeat creates a second campaign or webhook. Groups and fields refuse a second one with the same name (409 name_taken).
POST /campaigns/{id}/test No: each call sends test emails.

A 429 or a network error before an answer can be retried after the Retry-After time. Batch requests are not atomic: on an error, check each answer before sending the batch again.