> For the complete documentation index, see [llms.txt](https://docs.hackle.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hackle.io/crm-marketing/message-personalization/connected-content.md).

# 커넥티드 콘텐츠 (Connected Content) 작성하기

메시지를 보내는 그 순간, 외부 API에서 **실시간 데이터**를 가져와 본문에 끼워 넣는 기능입니다.\
잔여 포인트, 오늘의 쿠폰, 개인화 추천 상품처럼 Hackle에 저장돼 있지 않은 정보를 메시지에 활용할 수 있습니다.

<figure><img src="/files/BndFkoqFdL0LqzV6Zvhj" alt=""><figcaption></figcaption></figure>

#### 비개발자를 위한 커넥티드 콘텐츠 작성 방법

{% hint style="success" %}
코드를 직접 쓸 필요는 없습니다.\
메시지 편집기의 **`{...}` 개인화 변수 추가 버튼**을 누르면, 폼만 채우는 방식으로 `{% connected_content %}` 태그가 자동으로 만들어집니다.
{% endhint %}

{% hint style="info" %}
**시작 전 준비물 (개발팀에서 받아 두세요)**

1. 호출할 API 주소(`https://`로 시작)
2. 요청 방식(GET 또는 POST)
3. 응답 예시(JSON) - 어떤 필드를 메시지에 쓸 수 있는지 확인용
   {% endhint %}

{% stepper %}
{% step %}

### 커넥티드 콘텐츠 선택하기

메시지 편집기에서 `{...}` 버튼을 클릭합니다. **커넥티드 콘텐츠** 탭을 선택하세요.

모달 상단에서 **속성** 또는 **커넥티드 콘텐츠**를 선택합니다. **커넥티드 콘텐츠 — 외부 API에서 실시간 데이터**를 선택하세요.

<figure><img src="/files/T5ZBpMbl3Jm5CgZzQo6Q" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### API URL을 입력하고 테스트하기

**API URL**을 입력합니다. 인증 헤더나 POST 본문이 필요하면 **요청 옵션**을 펼쳐 입력합니다. URL에는 `{{user_properties["id"]}}` 같은 개인화 변수도 그대로 끼워 넣을 수 있습니다. 그다음 **`API 테스트`** 버튼을 눌러 실제 응답이 잘 오는지, 어떤 필드가 포함되어 있는지 미리 확인합니다.

<figure><img src="/files/c77kgkrhkXIkKijb2EUy" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### 메시지에 넣을 값과 변수명 선택하기

응답 결과에서 메시지에 노출할 필드(예: `code`, `discount`)를 체크합니다. 그리고 \*\*응답 변수명(`:save`)\*\*에 사용할 이름(예: `product`)을 입력합니다. 이 이름이 곧 본문에서 값을 꺼내 쓸 때 사용할 변수가 됩니다.

<figure><img src="/files/VH5lddSjCjIdFKNnzLN6" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### 저장한 변수를 본문에 삽입하기

`저장하기`를 누르면 모달이 만든 변수가 **"사용할 수 있는 변수" 목록**에 추가됩니다. 본문 원하는 위치에 커서를 두고 해당 변수를 클릭하면 `{{coupon.code}}`처럼 자동 삽입됩니다.

```liquid
{% connected_content https://dummyjson.com/products/1 :save product %}

찜하신 {{product.title}} 재고 {{product.stock}}개 남았어요!
지금 {{product.price}}원에 만나보세요
```

{% hint style="info" %}
응답이 비어 올 때를 대비해 **기본 문구**를 함께 넣어 두세요.
{% endhint %}

<figure><img src="/files/IEVFyGx3RQTnrBKyQXKc" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

### 기본 문법

```liquid
{% connected_content <URL>
    [:method <METHOD>]
    [:body <BODY>]
    [:headers <HEADERS_JSON>]
    [:save <VAR_NAME>] %}
```

<table><thead><tr><th width="121.3203125">구분</th><th width="494.12109375">설명</th><th>필수 여부</th></tr></thead><tbody><tr><td><code>&#x3C;URL></code></td><td><ul><li>호출할 API의 절대 URL입니다.</li><li><code>https://</code> 스킴만 사용할 수 있습니다.</li><li>공인 IP로 해석되는 최종 URL만 호출할 수 있습니다.</li></ul></td><td><strong>필수</strong></td></tr><tr><td><code>:method</code></td><td><ul><li>HTTP 메서드입니다.</li><li><code>GET</code>과 <code>POST</code>를 지원합니다.</li><li>기본값은 <code>GET</code>입니다.</li><li><code>PUT</code>, <code>DELETE</code>, <code>PATCH</code> 등은 태그 전체를 스킵합니다.</li></ul></td><td>선택</td></tr><tr><td><code>:body</code></td><td><ul><li>POST 요청 본문입니다.</li><li><code>:method POST</code>일 때만 사용합니다.</li><li>GET 요청에서는 무시됩니다.</li><li>JSON 객체 문자열을 입력하세요. 예: <code>:body {"foo":"bar"}</code></li></ul></td><td>선택</td></tr><tr><td><code>:headers</code></td><td><ul><li>요청 헤더입니다.</li><li>모든 값이 문자열인 JSON 객체를 입력하세요.</li><li>예: <code>:headers {"Authorization":"Bearer abc","X-Trace":"123"}</code></li><li><code>:headers {"Retry": 3}</code>처럼 숫자 값을 넣으면 검증에 실패해 태그를 스킵합니다.</li><li><code>Content-Type: application/json</code>은 기본 적용됩니다.</li></ul></td><td>선택</td></tr><tr><td><code>:save</code></td><td><ul><li>응답 JSON 객체를 저장할 변수 이름입니다.</li><li>이후 <code>{{변수.필드}}</code>로 사용할 수 있습니다.</li><li>같은 템플릿의 이후 태그와 Output 노드에서도 참조할 수 있습니다.</li><li>응답이 객체가 아니거나 비어 있으면 빈 값으로 저장됩니다.</li></ul></td><td>선택</td></tr></tbody></table>

{% hint style="success" %}
URL, body, headers의 값에 `{{user_properties["name"] | default: "고객님"}}` , `{{event_properties["campaign_id"] | default: "1"}}` 같은 개인화 변수를 자유롭게 사용 할 수 있습니다.
{% endhint %}

### 응답 처리 규칙

| 응답 형태                              | 결과                                                                                                                       |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `200 OK` + JSON 객체                 | <p>JSON을 파싱하여 <code>:save</code> 변수에 매핑. </p><p>필드는 <code>{{var.field}}</code> 또는 <code>{{var\["field"]}}</code>로 접근</p> |
| `200 OK` + JSON 배열 또는 원시값          | JSON 객체가 아니므로 빈 값으로 처리                                                                                                   |
| `200 OK` + 빈 본문                    | 빈 값으로 처리                                                                                                                 |
| 200이 아닌 모든 상태 코드 (4xx, 5xx, 3xx 등) | 빈 값으로 처리 (***리다이렉트 따라가지 않음***)                                                                                           |
| 응답 본문이 1MB 초과                      | 빈 값으로 처리                                                                                                                 |
| 네트워크 오류, 타임아웃                      | 빈 값으로 처리                                                                                                                 |

{% hint style="warning" %}
빈 값(empty) 으로 처리되었다는 것은 `:save`로 지정한 변수에 빈 맵이 저장되었다는 뜻입니다. `{{var.anything}}`은 빈 문자열로 렌더링되며, 이로 인해 메시지 발송 자체가 실패하지는 않습니다.
{% endhint %}

### 보안 정책

<table><thead><tr><th width="335.4296875">항목</th><th>한도</th></tr></thead><tbody><tr><td>URL 스킴</td><td><code>https://</code>만 허용</td></tr><tr><td>호스트</td><td>공인 IP로 해석되는 호스트만 허용<br>사내망 / 사설 대역은 차단됩니다.</td></tr><tr><td>리다이렉트</td><td><strong>따라가지 않음</strong><br>최종 URL을 직접 지정해야 합니다.</td></tr><tr><td>응답 본문 크기</td><td><strong>최대 1MB</strong><br>초과 시 빈 값으로 처리됩니다.</td></tr><tr><td>단일 호출 타임아웃</td><td><strong>2초</strong></td></tr><tr><td>한 메시지(Liquid render) 내부 누적 호출 시간</td><td><strong>총 2초</strong><br>한도 초과 후 호출은 HTTP 호출 없이 스킵됩니다.</td></tr><tr><td>HTTP 메서드</td><td><code>GET</code>, <code>POST</code>만</td></tr><tr><td>헤더 값 형식</td><td>JSON 객체 문자열, 값은 모두 문자열</td></tr></tbody></table>

### 실패 시 동작

Connected Content는 메시지 발송을 막지 않습니다. 모든 실패 케이스는 "***default 값으로 대체***" 됩니다.

**대표 케이스:**

* URL이 비어 있거나 플래그-값 쌍이 맞지 않는 경우 → 태그 전체 스킵
* `:method`에 `PUT` 같은 미지원 메서드를 지정 → 태그 스킵
* `:headers`가 유효한 JSON 객체 문자열이 아닌 경우 → 태그 스킵
* URL 검증 실패 (HTTPS 아님, 사설 IP 등) → 빈 응답
* 네트워크 오류, 200 외 응답, 본문 1MB 초과, JSON 객체 아님 → 빈 응답
* 한 메시지 내 누적 호출 시간 2초 초과 이후의 호출 → HTTP 호출 없이 스킵, 빈 응답

빈 응답이 저장된 변수를 메시지에서 사용해도 빈 문자열로 렌더링됩니다. 따라서 응답이 비어 있을 때를 대비해 다음과 같은 방어 코드를 곁들이는 것을 권장합니다.

```
{% connected_content https://api.example.com/coupon :save coupon %}
{% if coupon.code %}
오늘의 쿠폰: {{coupon.code}}
{% else %}
지금 바로 확인해 보세요!
{% endif %}
```

```
{% connected_content https://api.example.com/user :save user %}
{{ user.name | default: "고객" }}님 안녕하세요! ...
```

***

### 예제

#### 1. 가장 단순한 GET 호출

```liquid
{% connected_content https://api.example.com/products/123 :save product %}
{{product.name}} {{product.price}}원
```

응답이 `{"name":"운동화","price":59000}`이라면 → `운동화 59000원`

#### 2. URL에 사용자/이벤트 변수 끼워 넣기

```liquid
{% connected_content https://api.example.com/orders/{{event_properties["order_id"]}} :save order %}
주문 상태: {{order.status}}
```

`event_properties.order_id = "ABC-123"`이면 실제 호출 URL은 `https://api.example.com/orders/ABC-123`이 됩니다.

#### 3. POST + JSON body

```liquid
{% connected_content https://api.example.com/recommend :method POST
    :body {"user_id":"{{user_properties["id"]}}","limit":3}
    :save rec %}
추천 상품: {{rec.items[0].name}}
```

#### 4. 인증 헤더가 필요한 호출

```liquid
{% connected_content https://api.example.com/me
    :headers {"Authorization":"Bearer {{api_properties.token}}"}
    :save me %}
{{me.nickname}}님 안녕하세요
```

#### 5. 중첩된 JSON 응답에서 값 꺼내기

```liquid
{% connected_content https://api.example.com/products/456 :save data %}
{{data.product.name}} by {{data.product.brand}}
```

응답 `{"product":{"name":"티셔츠","brand":"Nike"}}` → `티셔츠 by Nike`

#### 6. 태그 체이닝 (앞 호출 결과를 다음 호출에 사용)

```liquid
{% connected_content https://api.example.com/me :save me %}
{% connected_content https://api.example.com/orders/{{me.id}} :save orders %}
{{me.id}}님의 주문 {{orders.total}}건
```

#### 7. POST + form-encoded 본문 + 커스텀 헤더

```liquid
{% connected_content https://api.example.com/post
    :method POST
    :headers {"X-Trace":"abc123","X-Src":"hackle"}
    :body {"a":1,"b"=3}
    :save r %}
{{r.ok}}
```

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.hackle.io/crm-marketing/message-personalization/connected-content.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
