> ## Documentation Index
> Fetch the complete documentation index at: https://docs.madiad.com/llms.txt
> Use this file to discover all available pages before exploring further.

# n8n

> Tự động đăng lên mọi nền tảng từ workflow n8n bằng REST API của MADIAD Hub.

n8n giao tiếp với MADIAD Hub qua HTTP thuần, không cần cài node đặc biệt nào. Bạn lưu API key một lần dưới dạng credential, sau đó điều khiển Hub bằng node **HTTP Request** trong bất kỳ workflow nào: đăng khi có dòng mới trong bảng tính, khi có bài RSS mới, theo lịch, hoặc từ ứng dụng của bạn.

<Note>
  Hướng dẫn này giả định bạn đã có API key và ít nhất một profile được kết nối. Nếu chưa, hãy làm [Bắt đầu nhanh](/vi/quickstart) trước (5 phút).
</Note>

## 1. Lưu API key thành credential

Giữ key ngoài file workflow bằng cách lưu nó thành credential dùng lại được.

1. Trong n8n mở **Credentials → New**, chọn **Header Auth**.
2. Đặt:
   * **Name** là `Authorization`
   * **Value** là `Bearer mdc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
3. Lưu lại với tên ví dụ **MADIAD Hub**.

Mỗi node HTTP Request bên dưới sẽ chọn credential này ở **Authentication → Generic Credential Type → Header Auth**.

<Warning>
  Tạo **một key riêng cho mỗi workflow** (đặt tên `n8n` trong dashboard). Nếu key bị lộ, bạn chỉ cần thu hồi đúng key đó mà không ảnh hưởng phần còn lại.
</Warning>

## 2. Đăng một bài chỉ có chữ

Thêm node **HTTP Request**:

| Trường         | Giá trị                                |
| -------------- | -------------------------------------- |
| Method         | `POST`                                 |
| URL            | `https://api.madiad.com/v1/posts/text` |
| Authentication | Header Auth → *MADIAD Hub*             |
| Send Body      | Bật, **JSON**                          |

Body (JSON):

```json theme={null}
{
  "profile_id": "prof_01HZX9F2K4M7N6QR8T0V2W4Y6Z",
  "platforms": ["facebook", "linkedin", "x"],
  "caption": "Bài viết mới đã lên sóng, đọc ngay trên blog."
}
```

Bài chỉ có chữ dùng JSON. Ảnh có thể gửi bằng file upload (multipart) hoặc URL ảnh công khai (JSON); video luôn dùng JSON với URL công khai, xem [Đăng ảnh hoặc video](#4-đăng-ảnh-hoặc-video) bên dưới.

## 3. Chống trùng lặp bằng Idempotency-Key

Nếu n8n chạy lại một node (timeout, nhánh lỗi), bạn không muốn tạo ra bài đăng trùng. Thêm một header có giá trị ổn định cho mỗi item, Hub sẽ coi lần lặp lại cùng key là cùng một yêu cầu.

Ở phần **Headers**, thêm:

| Name              | Value             |
| ----------------- | ----------------- |
| `Idempotency-Key` | `={{ $json.id }}` |

Dùng một giá trị **duy nhất cho mỗi bài định đăng** và **không đổi khi thử lại chính bài đó**. Lựa chọn tốt: một UUID bạn sinh một lần rồi ghi ngược lại vào dòng dữ liệu, hoặc một hash của chính nội dung.

<Warning>
  **Tuyệt đối không dùng giá trị bị tái sử dụng.** Số dòng bảng tính sẽ dịch chuyển khi bạn xoá dòng, còn `$itemIndex` quay về 0 ở mỗi lần chạy. Cả hai đều khiến nội dung hôm nay mang một key mà bài hôm qua đã chiếm. Khi gặp key đã dùng, Hub trả về kết quả **cũ** và không đăng gì cả, nên một key hỏng trông y hệt một bài đăng thành công. Hãy để ý header `Idempotent-Replayed: true`, và xem [Idempotency](/vi/concepts/idempotency).
</Warning>

## 4. Đăng ảnh hoặc video

Endpoint là `https://api.madiad.com/v1/posts/photos` cho ảnh và `https://api.madiad.com/v1/posts/video` cho một video.

**Ảnh, tải file lên**: chọn **Send Body → Form-Data Multipart** rồi thêm từng trường thành từng entry riêng. Lặp lại `platforms[]` một lần cho mỗi nền tảng, và gửi ảnh dưới dạng trường **binary**:

| Tên trường    | Kiểu   | Giá trị                                      |
| ------------- | ------ | -------------------------------------------- |
| `profile_id`  | Text   | `prof_…`                                     |
| `platforms[]` | Text   | `instagram`                                  |
| `platforms[]` | Text   | `facebook`                                   |
| `caption`     | Text   | `Ra mắt hương vị mới hôm nay`                |
| `photos[]`    | Binary | binary property từ node trước (ví dụ `data`) |

**Ảnh, dùng URL công khai**: nếu ảnh đã có link `https://` công khai, bỏ qua bước xử lý file luôn. Chọn **Send Body → JSON** và truyền link vào `photo_urls[]`, đây là cách đơn giản nhất cho n8n vì không cần node binary nào:

```json theme={null}
{
  "profile_id": "prof_01HZX9F2K4M7N6QR8T0V2W4Y6Z",
  "platforms": ["instagram", "facebook"],
  "caption": "Ra mắt hương vị mới hôm nay",
  "photo_urls": ["https://cdn.example.com/launch.jpg"]
}
```

**Video**: luôn dùng JSON, không dùng multipart. Chọn **Send Body → JSON** và truyền một `video_url` công khai:

```json theme={null}
{
  "profile_id": "prof_01HZX9F2K4M7N6QR8T0V2W4Y6Z",
  "platforms": ["facebook", "instagram"],
  "caption": "Ra mắt hương vị mới hôm nay",
  "video_url": "https://cdn.example.com/launch.mp4"
}
```

<Tip>
  Cần tải file ảnh lên thay vì dùng URL? Đặt một node **HTTP Request** (Response Format → *File*) hoặc node **Read/Download** trước node này, rồi tham chiếu binary property của nó trong `photos[]`.
</Tip>

## 5. Xử lý phản hồi bất đồng bộ

Bài chỉ có chữ hoặc bài ảnh tới các nền tảng nhanh thường trả về `"status": "completed"` kèm `results` theo từng nền tảng. **Video là bất đồng bộ**, nó trả về `"status": "processing"` và một `request_id`. Có hai cách lấy kết quả cuối:

**Poll** là thêm node **Wait**, rồi node **HTTP Request**:

```
GET https://api.madiad.com/v1/posts/status?request_id={{ $json.request_id }}
```

Lặp cho tới khi `status` là `completed`, `partial`, hoặc `failed`.

**Webhook (khuyến nghị)** là nhận thông báo đẩy thay vì poll. Thêm node **Webhook** trong n8n, sao chép Production URL của nó, và đăng ký trong dashboard tại [hub.madiad.com/dashboard/webhooks](https://hub.madiad.com/dashboard/webhooks) cho sự kiện `post.completed`. Hub ký mọi lần gửi bằng HMAC-SHA256, hãy xác thực chữ ký ở node kế tiếp. Xem [Webhooks](/vi/concepts/webhooks).

## 6. Đọc kết quả theo từng nền tảng

Một bài đăng có thể thành công một phần, hãy kiểm tra `results` theo từng nền tảng thay vì chỉ nhìn `status` tổng:

```json theme={null}
{
  "status": "partial",
  "results": {
    "facebook": { "success": true,  "url": "https://facebook.com/12345/posts/67890" },
    "x":        { "success": false, "error": "Upload rejected by the platform" }
  },
  "failed_platforms": ["x"]
}
```

Rẽ nhánh theo `success` bằng node **IF** để chuyển các lỗi sang một cảnh báo Slack hoặc email.

## 7. Xử lý lỗi mà không retry vô tận

**Retry On Fail** của n8n coi mọi phản hồi không phải 2xx là đáng thử lại, và điều đó sai với phần
lớn lỗi ở đây. Hãy rẽ nhánh theo mã trạng thái HTTP, đừng dựa vào câu chữ của thông báo:

| Trạng thái                    | Nghĩa là gì                                                           | Workflow nên làm gì                                                                                                                |
| ----------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `400`                         | Nội dung hoặc request sai (caption vượt giới hạn nền tảng, URL hỏng). | Dừng lại. Gửi lại y nguyên thì không thể thành công, hãy đẩy sang nhánh lỗi.                                                       |
| `409 platform_not_connected`  | Profile chưa có tài khoản cho nền tảng đó.                            | Báo cho người thật. Nếu cứ sau mỗi bài đăng được là lại gặp, nền tảng đang hạn chế tài khoản và kết nối lại không giải quyết được. |
| `409 idempotency_in_progress` | Lần gửi trước với cùng key vẫn đang chạy.                             | Đợi vài giây rồi thử lại **với đúng key cũ**, bạn sẽ nhận kết quả của lần gửi đầu.                                                 |
| `429`                         | Bị giới hạn tốc độ.                                                   | Chờ đủ số giây trong `Retry-After` rồi thử lại.                                                                                    |
| `5xx`                         | Lỗi phía Hub hoặc phía backend xuất bản.                              | Thử lại kèm backoff, và **luôn gửi `Idempotency-Key`** để lần retry không tạo bài trùng.                                           |

Trong node HTTP Request, hãy bật **Never Error** (hoặc đặt *On Error* thành *Continue using error
output*) để đọc được `$json.error.code` và rẽ nhánh theo nó, thay vì để node làm hỏng cả lượt chạy.

<Warning>
  Trước ngày 07/08/2026, mọi lỗi từ backend xuất bản đều trả về `502`, nên một workflow bật
  **Retry On Fail** sẽ retry mãi những bài không bao giờ đăng được. Nếu workflow của bạn đang có
  vòng retry dựa trên hành vi cũ đó, hãy thay bằng bảng ở trên.
</Warning>

## Ví dụ workflow: Google Sheets → mọi nền tảng

Một mẫu phổ biến là lịch nội dung trong Google Sheets, tự đăng khi một dòng được đánh dấu sẵn sàng:

1. **Schedule Trigger** (mỗi 15 phút) → **Google Sheets** (lấy các dòng có `status = ready`).
2. **HTTP Request** → `POST /v1/posts/photos` với `profile_id`, `platforms[]`, `caption` lấy từ dòng đó, `Idempotency-Key` = một UUID lưu sẵn trên dòng đó (không dùng số dòng, xem cảnh báo ở trên).
3. **IF** theo phản hồi → nếu thành công, **Google Sheets** cập nhật dòng thành `published`; nếu lỗi, gửi cảnh báo.

## Bước tiếp theo

<CardGroup cols={2}>
  <Card title="Tuỳ chọn theo nền tảng" icon="sliders" href="/vi/concepts/posts">
    Ghi đè caption, tiêu đề và tuỳ chọn cho từng nền tảng.
  </Card>

  <Card title="Lên lịch" icon="calendar" href="/vi/concepts/scheduling">
    Để Hub tự đăng vào thời điểm tương lai.
  </Card>

  <Card title="Idempotency" icon="shield-check" href="/vi/concepts/idempotency">
    Chạy lại an toàn từ mọi automation.
  </Card>

  <Card title="Webhooks" icon="bolt" href="/vi/concepts/webhooks">
    Nhận callback có chữ ký khi bài đăng hoàn tất.
  </Card>
</CardGroup>
