> ## 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

> Auto-publish to every platform from an n8n workflow using the MADIAD Hub REST API.

n8n talks to MADIAD Hub through plain HTTP — there is no special node to install. You store your API key once as a credential, then drive the Hub from an **HTTP Request** node inside any workflow: publish when a new row lands in a sheet, when an RSS item appears, on a schedule, or from your own app.

<Note>
  This guide assumes you already have an API key and at least one connected profile. If not, do the [Quickstart](/quickstart) first (5 minutes).
</Note>

## 1. Store the API key as a credential

Keep the key out of your workflow JSON by saving it as a reusable credential.

1. In n8n open **Credentials → New**, and choose **Header Auth**.
2. Set:
   * **Name** — `Authorization`
   * **Value** — `Bearer mdc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
3. Save it as, for example, **MADIAD Hub**.

Every HTTP Request node below selects this credential under **Authentication → Generic Credential Type → Header Auth**.

<Warning>
  Create a **separate key per workflow** (name it `n8n` in your dashboard). If it ever leaks, you revoke that one key without breaking anything else.
</Warning>

## 2. Publish a text post

Add an **HTTP Request** node:

| Field          | Value                                  |
| -------------- | -------------------------------------- |
| Method         | `POST`                                 |
| URL            | `https://api.madiad.com/v1/posts/text` |
| Authentication | Header Auth → *MADIAD Hub*             |
| Send Body      | On, **JSON**                           |

Body (JSON):

```json theme={null}
{
  "profile_id": "prof_01HZX9F2K4M7N6QR8T0V2W4Y6Z",
  "platforms": ["facebook", "linkedin", "x"],
  "caption": "New article is live — read it on the blog."
}
```

Text posts use JSON. Photos support either a file upload (multipart) or a public image URL (JSON); video always uses JSON with a public URL — see [Publish photos or video](#4-publish-photos-or-video) below.

## 3. Make retries safe with an Idempotency-Key

If n8n retries a node (timeout, error path), you don't want a duplicate post. Add a header whose value is stable for a given item — the Hub treats a repeat of the same key as the same request.

Under **Headers**, add:

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

Use a value that is **unique per intended post** and **constant across retries of that same post**. Good choices: a UUID you generate once and write back to the row, or a hash of the content itself.

<Warning>
  **Never use a value that gets reused.** A spreadsheet row number shifts when rows are deleted, and `$itemIndex` restarts at 0 on every execution — both hand tomorrow's content a key that yesterday's post already claimed. The Hub answers a reused key with the ORIGINAL result and publishes nothing, so a broken key looks exactly like a successful post. Watch for the `Idempotent-Replayed: true` header, and see [Idempotency](/concepts/idempotency).
</Warning>

## 4. Publish photos or video

The endpoint is `https://api.madiad.com/v1/posts/photos` for images and `https://api.madiad.com/v1/posts/video` for a single video.

**Photos — upload the file**: set **Send Body → Form-Data Multipart** and add the fields as separate entries. Repeat `platforms[]` once per platform, and send the image as a **binary** field:

| Field name    | Type   | Value                                                  |
| ------------- | ------ | ------------------------------------------------------ |
| `profile_id`  | Text   | `prof_…`                                               |
| `platforms[]` | Text   | `instagram`                                            |
| `platforms[]` | Text   | `facebook`                                             |
| `caption`     | Text   | `New blend dropping today`                             |
| `photos[]`    | Binary | the binary property from a previous node (e.g. `data`) |

**Photos — send a public URL instead**: if the image already has a public `https://` link, skip file handling entirely. Set **Send Body → JSON** and pass the link in `photo_urls[]` — this is the simplest option for n8n since no binary node is needed:

```json theme={null}
{
  "profile_id": "prof_01HZX9F2K4M7N6QR8T0V2W4Y6Z",
  "platforms": ["instagram", "facebook"],
  "caption": "New blend dropping today",
  "photo_urls": ["https://cdn.example.com/launch.jpg"]
}
```

**Video**: always JSON, no multipart. Set **Send Body → JSON** and pass a public `video_url`:

```json theme={null}
{
  "profile_id": "prof_01HZX9F2K4M7N6QR8T0V2W4Y6Z",
  "platforms": ["facebook", "instagram"],
  "caption": "New blend dropping today",
  "video_url": "https://cdn.example.com/launch.mp4"
}
```

<Tip>
  Need to upload a photo file instead of using a URL? Put an **HTTP Request** node (Response Format → *File*) or a **Read/Download** node before this one, then reference its binary property in `photos[]`.
</Tip>

## 5. Handle the async response

A text or photo post to fast platforms usually returns `"status": "completed"` with per-platform `results`. **Video is asynchronous** — it returns `"status": "processing"` and a `request_id`. Two ways to get the final result:

**Poll** — add a **Wait** node, then an **HTTP Request**:

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

Loop until `status` is `completed`, `partial`, or `failed`.

**Webhook (recommended)** — instead of polling, receive a push. Add a **Webhook** node in n8n, copy its Production URL, and register it in your dashboard at [hub.madiad.com/dashboard/webhooks](https://hub.madiad.com/dashboard/webhooks) for the `post.completed` event. The Hub signs every delivery with HMAC-SHA256 — verify the signature in a following node. See [Webhooks](/concepts/webhooks).

## 6. Read per-platform results

A post can partially succeed — inspect `results` per platform, not just the top-level `status`:

```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"]
}
```

Branch on `success` with an **IF** node to route failures to a Slack/email alert.

## 7. Handle errors without retrying forever

n8n's **Retry On Fail** treats any non-2xx as worth another attempt, which is wrong for most
failures here. Branch on the HTTP status code instead — never on the wording of the message:

| Status                        | What it means                                                                   | What the workflow should do                                                                                                             |
| ----------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `400`                         | The content or the request is wrong (caption over a platform's limit, bad URL). | Stop. Retrying the same item cannot succeed — send it to an error branch.                                                               |
| `409 platform_not_connected`  | The Profile has no account for that platform.                                   | Alert a human. If it recurs right after each successful post, the platform is restricting the account and reconnecting will not fix it. |
| `409 idempotency_in_progress` | An earlier attempt with the same key is still running.                          | Wait a few seconds and retry **with the same key** — it returns the first attempt's result.                                             |
| `429`                         | Rate limited.                                                                   | Back off for `Retry-After` seconds, then retry.                                                                                         |
| `5xx`                         | Our side or the publishing backend failed.                                      | Retry with backoff, **always with an `Idempotency-Key`** so the retry cannot double-post.                                               |

In the HTTP Request node, turn on **Never Error** (or set *On Error* to *Continue using error
output*) so you can read `$json.error.code` and route on it, rather than letting the node abort the
run.

<Warning>
  Before 2026-08-07 every publishing-backend failure came back as `502`, so a workflow with
  **Retry On Fail** enabled would retry permanently-broken posts indefinitely. If yours has a
  retry loop built on that behaviour, replace it with the table above.
</Warning>

## Example workflow: Google Sheets → all platforms

A common pattern — a content calendar in Google Sheets, auto-published when a row is marked ready:

1. **Schedule Trigger** (every 15 min) → **Google Sheets** (get rows where `status = ready`).
2. **HTTP Request** → `POST /v1/posts/photos` with `profile_id`, `platforms[]`, `caption` from the row, `Idempotency-Key` = a UUID stored on the row (not the row number — see the warning above).
3. **IF** on the response → on success, **Google Sheets** update the row to `published`; on failure, send an alert.

## Next steps

<CardGroup cols={2}>
  <Card title="Per-platform fields" icon="sliders" href="/concepts/posts">
    Override captions, titles, and options per platform.
  </Card>

  <Card title="Scheduling" icon="calendar" href="/concepts/scheduling">
    Let the Hub publish at a future time for you.
  </Card>

  <Card title="Idempotency" icon="shield-check" href="/concepts/idempotency">
    Safe retries from any automation.
  </Card>

  <Card title="Webhooks" icon="bolt" href="/concepts/webhooks">
    Get a signed callback when a post finishes.
  </Card>
</CardGroup>
