Skip to content
Personalization

External Data

Render data from your own API inside a flow screen, filtered by the answers a user just gave.

External Data

A Data Source connects a flow screen to a GET endpoint on your API. Register the endpoint once, bind it into a screen, and the screen renders your data.

The parameters can come from answers given earlier in the same flow. Ask for a training goal on screen 1 and days per week on screen 2, and screen 3 shows the matching plans from your catalogue.

Setgreet calls your API from its servers, not from the device. Your endpoint needs to be reachable over HTTPS from the public internet.

When to use it

Variable bindings insert data Setgreet already has. External Data fetches data only you have.

You want to showUse
The user's first nameVariable Bindings
Their answer from an earlier screenExternal Data, or a Branch node
Live prices, inventory, or a product listExternal Data
A list whose length you do not knowExternal Data with a List component

Creating a Data Source

Create one from the Data Sources settings, or ask the AI agent to. You will need:

  • A key, lowercase with underscores. This is the name you bind against: a key of workout_plans is referenced as {{data.workout_plans...}}.
  • A URL, HTTPS only. It can contain path parameters.
  • Parameters, each with a name, a location (query or path), an optional default, and an optional validation pattern.
  • Authentication, if your endpoint needs it. See below.

Then run Test. Setgreet calls your endpoint once and shows you the response along with a field tree, so you can pick the exact paths to bind rather than typing them from memory.

Get in the habit of using Test. A mistyped field path does not error at render time, it renders an empty string, which reads like missing data rather than a typo.

Binding data into a screen

Open a screen and add the Data Source under Data. Give it an alias, and map each parameter to a value:

Parameter sourceSyntaxResolves to
A user attribute{{user.plan}}The attribute you set via identifyUser.
An earlier answer{{input.goal}}The input named goal in this flow.
An event property{{event.source}}A property on the event that triggered the flow.
A fixed valuebeginnerItself.

Then reference the response anywhere a component takes text:

{{data.workout_plans.results[0].title}}
{{data.workout_plans.total}}

Rendering a list

Add a List component, point its Items at an array in the response, and build one card inside it. That card is a template. Setgreet repeats it once per row, and {{item.field}} inside it refers to the current row.

Items:  {{data.workout_plans.results}}

  Card
    Text   {{item.name}}
    Text   {{item.price}}
    Image  {{item.image_url}}

A List belongs on a screen with Scrollable layout. On a Fit Screen layout the rows are squashed into whatever height is left over, and publishing is blocked.

Filling a dropdown from your API

Any dropdown, quiz, radio group, multi-select, chip group or segmented control can take its options from a Data Source instead of a hand-written list. Set the options source to the array, and pick which field is the label and which is the value.

Leave both the label and value fields empty when each row of your array is a plain value rather than an object, such as ["Small", "Medium", "Large"]. Setting only one of the two will not work: the options come out unreadable or empty, and publishing tells you so.

One thing to watch: you cannot know at design time how many options your API will return. If the screen's layout is Fit screen and its button is anchored to the bottom, a long list pushes that button off the edge where nobody can tap it. Set the screen's layout to Scrollable. Publishing blocks this combination rather than letting it ship.

The contract your API needs to meet

Setgreet only ever sends GET requests. It never writes to your API.

Respond with JSON. The content type must be application/json (or application/*+json). HTML error pages are rejected rather than parsed.

Respond within the timeout. Four seconds by default, ten maximum. Screens with external data are on the render path, so a slow endpoint is a slow screen.

Keep it under the size cap. 256KB by default, 1MB maximum. Paginate or filter rather than returning your whole catalog.

Respond 2xx. Any other status counts as a failure.

Failures degrade, they do not blank

If your API is slow, down, or returns something unusable, the screen still renders. It renders without the data-bound parts. The flow continues.

Repeated failures pause the Data Source temporarily (5 minutes, doubling up to 24 hours) so a broken endpoint does not slow every screen that binds it. Fix the endpoint and unpause the source, or wait for the automatic retry.

A flow author can opt into skipping a screen entirely when a required source fails, instead of showing it half-empty.

Authentication

Setgreet signs every call with a shared secret and tells you which end user it is for. You verify the signature and look the user up yourself. No user credential is ever stored at Setgreet.

Three headers arrive with each request:

HeaderContains
X-Setgreet-TimestampUnix seconds when the request was signed.
X-Setgreet-End-User-IdThe end user id you passed to identifyUser.
X-Setgreet-SignatureHex HMAC-SHA256 over timestamp, method, path, query.

The signed string is four newline-separated parts:

<timestamp>\n
GET\n
<path>\n
<sorted query>

timestamp is Unix seconds (not milliseconds), path excludes the query string, and sorted query is key=value pairs joined by &, sorted by key, exactly as they appear on the wire. The signature is the lowercase hex HMAC-SHA256 of that string using your secret.

import crypto from 'node:crypto';

const MAX_SKEW_SECONDS = 300;

export function verifySetgreetRequest(req, secret) {
  const timestamp = req.get('X-Setgreet-Timestamp');
  const signature = req.get('X-Setgreet-Signature');
  if (!timestamp || !signature) return null;

  // Reject old requests so a captured URL cannot be replayed forever.
  const nowSeconds = Math.floor(Date.now() / 1000);
  if (Math.abs(nowSeconds - Number(timestamp)) > MAX_SKEW_SECONDS) return null;

  const url = new URL(req.originalUrl, 'https://placeholder');
  const sortedQuery = [...url.searchParams.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([k, v]) => `${k}=${v}`)
    .join('&');

  const canonical = `${timestamp}\nGET\n${url.pathname}\n${sortedQuery}`;
  const expected = crypto.createHmac('sha256', secret).update(canonical).digest('hex');

  // Constant-time compare. A plain === leaks the signature one byte at a time.
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(signature, 'utf8');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;

  return req.get('X-Setgreet-End-User-Id');
}
import hmac, hashlib, time
from urllib.parse import urlparse, parse_qsl

MAX_SKEW_SECONDS = 300

def verify_setgreet_request(full_url, headers, secret):
    timestamp = headers.get("X-Setgreet-Timestamp")
    signature = headers.get("X-Setgreet-Signature")
    if not timestamp or not signature:
        return None

    # Reject old requests so a captured URL cannot be replayed forever.
    if abs(int(time.time()) - int(timestamp)) > MAX_SKEW_SECONDS:
        return None

    parsed = urlparse(full_url)
    sorted_query = "&".join(
        f"{k}={v}" for k, v in sorted(parse_qsl(parsed.query), key=lambda kv: kv[0])
    )

    canonical = f"{timestamp}\nGET\n{parsed.path}\n{sorted_query}"
    expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()

    # Constant-time compare. A plain == leaks the signature one byte at a time.
    if not hmac.compare_digest(expected, signature):
        return None

    return headers.get("X-Setgreet-End-User-Id")
require "openssl"
require "uri"

MAX_SKEW_SECONDS = 300

def verify_setgreet_request(full_url, headers, secret)
  timestamp = headers["X-Setgreet-Timestamp"]
  signature = headers["X-Setgreet-Signature"]
  return nil unless timestamp && signature

  # Reject old requests so a captured URL cannot be replayed forever.
  return nil if (Time.now.to_i - timestamp.to_i).abs > MAX_SKEW_SECONDS

  uri = URI.parse(full_url)
  sorted_query = URI.decode_www_form(uri.query || "")
                    .sort_by(&:first)
                    .map { |k, v| "#{k}=#{v}" }
                    .join("&")

  canonical = "#{timestamp}\nGET\n#{uri.path}\n#{sorted_query}"
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, canonical)

  # Constant-time compare. A plain == leaks the signature one byte at a time.
  return nil unless OpenSSL.secure_compare(expected, signature)

  headers["X-Setgreet-End-User-Id"]
end

Compare signatures in constant time and reject stale timestamps. Both lines are in the samples above for a reason.

Stored credentials

If signing is more than you want to set up, Setgreet can send a static credential instead: an API key (in a header or a query parameter), a bearer token, or basic auth. Credentials are encrypted at rest and never returned by the API or shown in the dashboard again after you save them. You will only ever see the last four characters.

Setgreet drops the credential if your endpoint redirects to a different host, so a misconfigured redirect cannot leak it.

What Setgreet will not call

Data Source URLs are checked before every request, including after each redirect:

  • HTTPS only, port 443 only.
  • No private, loopback, link-local or cloud metadata addresses. This covers the IPv6 and encoded forms of them too.
  • No credentials embedded in the URL.
  • At most two redirects, each fully re-checked.

The address is verified at the moment the connection opens, not just when you save the URL, so a hostname that resolves to a private address later is still blocked.

Values users type are not trusted

When a parameter comes from something the user typed, it is length-capped, checked against your pattern if you set one, and URL-encoded. Path parameters additionally reject /, \, ?, #, .. and % after decoding, so a user typing ../../admin into a text field cannot rewrite the path of your endpoint.

Setting a validation pattern on every user-supplied parameter is worth the minute it takes.

Limits

LimitDefaultMaximum
Timeout (per source)4s10s
Response size256KB1MB
Rows rendered by List2050
Cache lifetime5min1 hour
Sources fetched per screen4 at a time4 at a time
Combined budget per screen2.5s2.5s

Responses can be cached, globally or per user, for up to an hour (5 minutes by default). Failed responses are never cached, so a brief outage on your side does not become a long one in your flows.

Availability

External Data is available on Launch, Growth and Scale plans.

Next steps

On this page