Writing Connected Content
This is a feature that pulls real-time data from an external API at the very moment a message is sent and inserts it into the body. You can use information that is not stored in Hackle, such as remaining points, today's coupon, and personalized recommended products, in your messages.
How to write Connected Content for non-developers
You do not need to write code directly. When you press the {...} Add personalization variable button in the message editor, the {% connected_content %} tag is automatically generated just by filling out a form.
Before you start (get these from your development team)
The API address to call (starts with
https://)The request method (GET or POST)
A response example (JSON) - to check which fields can be used in the message
First, in the message editor, click the {...} button → select the "Connected Content" tab.
When the modal opens, select "Connected Content — real-time data from an external API" from Property / Connected Content at the top.

STEP 1. Enter the API URL and click Test API
Enter the API URL. If you need authentication headers or a POST body, expand Request Options and enter them. You can insert personalization variables such as {{user_properties["id"]}} directly into the URL as well. Then, press the Test API button to check in advance whether the actual response comes back correctly and which fields are included.

STEP 2. Select the values to insert into the message + enter a variable name
From the response result, check the fields to display in the message (e.g., code, discount). Then, enter the name (e.g., product) to use for the **response variable name (:save)**. This name becomes the variable you will use to pull values out in the body.

STEP 3. Click Save → insert by clicking the variable in the body
When you press Save, the variable created by the modal is added to the "Available variables" list. Place the cursor at the desired position in the body and click that variable, and it will be inserted automatically like {{coupon.code}}.

Basic Syntax
<URL>(required): The absolute URL of the external endpoint to call. Onlyhttps://is allowed.:method: The HTTP method. OnlyGET/POSTare supported, defaultGET.:body: The POST request body. Must be written as a JSON object string.:headers: The request headers. Must be written as a JSON object string, and all values must be strings. If not specified,Content-Type: application/jsonis applied by default.:save: The name of the variable to hold the response JSON. You can then pull it out in the message in the form{{variable.field}}. If not specified, the response cannot be used.
You can freely use personalization variables such as
{{user_properties["name"] | default: "Customer"}}and{{event_properties["campaign_id"] | default: "1"}}in the values of the URL, body, and headers.

Parameters
URL
Must be an absolute URL, and only the
https://scheme is allowed.Only final URLs that resolve to a public IP can be called.
:method
Supported:
GET,POST, defaultGETOther methods (
PUT,DELETE,PATCH, etc.) are rejected, and the entire tag is skipped.
:body
Used when
:method POST. It is ignored forGET.You can write inline JSON directly. Example:
:body {"foo":"bar"}
:headers
Must be a JSON object string, and all values must be strings.
Good example:
:headers {"Authorization":"Bearer abc","X-Trace":"123"}Bad example:
:headers {"Retry": 3}(value is a number) → validation fails, tag is skipped
Only the
Content-Type: application/jsontype is supported, and it is applied automatically as the default even if not specified.
:save
Stores the response JSON object in the context as a variable with this name.
It can be referenced by all subsequent tags / Output nodes within the same template. For example, you can reuse the result of the first
connected_contentin the URL or body of the secondconnected_content.If the response is not a JSON object or is empty, it is stored as an empty value (renders as an empty string).
Examples
1. The simplest GET call
If the response is {"name":"Sneakers","price":59000} → Sneakers 59000 won
2. Inserting user/event variables into the URL
If event_properties.order_id = "ABC-123", the actual call URL becomes https://api.example.com/orders/ABC-123.
3. POST + JSON body
4. A call that requires an authentication header
5. Extracting a value from a nested JSON response
Response {"product":{"name":"T-shirt","brand":"Nike"}} → T-shirt by Nike
6. Tag chaining (using the result of a previous call in the next call)
7. POST + form-encoded body + custom headers
Response Handling Rules
200 OK + JSON object
Parses the JSON and maps it to the :save variable. Fields are accessed via {{var.field}} or {{var["field"]}}
200 OK + JSON array or primitive value
Not a JSON object, so treated as an empty value
200 OK + empty body
Treated as an empty value
All status codes other than 200 (4xx, 5xx, 3xx, etc.)
Treated as an empty value (redirects are not followed)
Response body exceeds 1MB
Treated as an empty value
Network error, timeout
Treated as an empty value
Being treated as an empty value means that an empty map is stored in the variable specified by
:save.{{var.anything}}renders as an empty string, and this does not cause the message send itself to fail.
Constraints and Security Policy
Protective Limits
URL scheme
Only https:// allowed
Host
Only hosts that resolve to a public IP allowed (internal network / private ranges blocked)
Redirects
Not followed (you must specify the final URL directly)
Response body size
Up to 1MB (treated as empty value if exceeded)
Single call timeout
2 seconds
Cumulative call time within one message (Liquid render)
2 seconds total (calls after the limit is exceeded are skipped without an HTTP call)
HTTP methods
GET, POST only
Header value format
JSON object string, all values must be strings
Behavior on Failure
Connected Content does not block message sending. All failure cases are "replaced with the default value."
Representative cases:
URL is empty or a flag-value pair does not match → entire tag is skipped
An unsupported method such as
PUTis specified in:method→ tag is skipped:headersis not a valid JSON object string → tag is skippedURL validation fails (not HTTPS, private IP, etc.) → empty response
Network error, non-200 response, body exceeds 1MB, not a JSON object → empty response
Calls after the cumulative call time within one message exceeds 2 seconds → skipped without an HTTP call, empty response
Even if you use a variable in which an empty response is stored in the message, it renders as an empty string. Therefore, we recommend adding defensive code like the following in case the response is empty.
FAQ
Q. Can I use connected_content multiple times in one message?
Yes. However, all connected_content calls in one message share a cumulative time budget of 2 seconds. If the first call took 1.8 seconds, the second call must finish within 0.2 seconds, and if the limit is exceeded in between, subsequent calls are immediately skipped without a network call. Use multiple calls only when the average response time of the external API is sufficiently short.
Q. Won't it take a long time to send while waiting for the response?
Each call terminates within a maximum of 2 seconds. In addition, since the cumulative limit for one message is 2 seconds, send delays do not accumulate indefinitely.
Q. How do I access the response when it is a JSON array?
If the top level is an array, it is not recognized as an object and becomes an empty value. Have the API side return it wrapped in an object, such as {"items":[...]}, or set up a separate wrapping endpoint.
Q. Can I use non-JSON responses (e.g., plain text, HTML)?
Not supported. If it cannot be parsed as a JSON object, it is treated as an empty value.
Last updated