For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

lightbulb

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)

  1. The API address to call (starts with https://)

  2. The request method (GET or POST)

  3. 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}}.

Tips for using it safely

In case the response comes back empty, include a fallback text as well.

Basic Syntax

  • <URL> (required): The absolute URL of the external endpoint to call. Only https:// is allowed.

  • :method : The HTTP method. Only GET / POST are supported, default GET.

  • :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/json is 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, default GET

  • Other methods (PUT, DELETE, PATCH, etc.) are rejected, and the entire tag is skipped.

:body

  • Used when :method POST. It is ignored for GET.

  • 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/json type 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_content in the URL or body of the second connected_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

Response form
Result

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

Item
Limit

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 PUT is specified in :method → tag is skipped

  • :headers is not a valid JSON object string → tag is skipped

  • URL 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.

Q. I want to use the response only for conditional branching without displaying it directly in the message.

You can use the :save variable directly in {% if %} and the like, and you do not need to output it in the message body. Refer to the examples in the "Behavior on Failure" section above.

Q. How do I extract a response field whose key contains Korean characters, spaces, or dots?

Use bracket subscript in the form {{var["key name"]}}. Single quotes (') can also be used.

Last updated