{
  "info": {
    "name": "Commune: Publish from wherever you write",
    "description": "Your writing already lives somewhere: a folder of Markdown files, a CMS, a notes app. Copying it into another editor every week is where typos creep in and send times slip. Commune takes the Markdown as it is, lets you check it and send yourself a test, then sends it to your list on time and tells you how it did.\n\nThe walkthrough: https://usecommune.dev/use-cases/publish-from-anywhere\n\nSet the `apiKey` variable to a key from https://usecommune.com/settings/api-keys, then run the requests in order.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.usecommune.com"
    },
    {
      "key": "apiKey",
      "value": ""
    },
    {
      "key": "version",
      "value": "2026-08-26"
    },
    {
      "key": "newsletter",
      "value": "example-letter"
    },
    {
      "key": "article",
      "value": "8f1b7c44-2a19-4d90-b7e2-51d6c3a8f012"
    }
  ],
  "item": [
    {
      "name": "1. Find your newsletter",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/newsletters",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "newsletters"
          ]
        },
        "description": "A key reaches one newsletter or several, whichever its owner ticked when creating it. List them and pick yours by `handle` (or `id`, either works in a path) for the requests below, and check `esp`: only `commune` can be written to and sent from here."
      }
    },
    {
      "name": "2. Create the article from Markdown",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{$guid}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/newsletters/{{newsletter}}/articles",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "newsletters",
            "{{newsletter}}",
            "articles"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"title\": \"What we learned in March\",\n  \"preview_text\": \"The third one surprised us.\",\n  \"slug\": \"what-we-learned-in-march\",\n  \"content_markdown\": \"# What we learned in March\\n\\nThree things, and the **third** one surprised us.\\n\\n<EmailOnly>\\n[Unsubscribe]({{ unsubscribe_url }}) | {{ address }}\\n</EmailOnly>\\n\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "The body goes in `content_markdown`, as Markdown: headings, emphasis, links, images, quotes, lists, code, tables. HTML is not accepted, and raw HTML inside the Markdown is refused rather than passed through (write a literal `<` as `\\<`). Commune parses it strictly: a line it cannot read answers `400` naming the line, and nothing is written. Merge tags such as `{{ subscriber.first_name }}` are kept exactly as written.\n\nThe body is stored exactly as sent, so it has to carry its own footer: an unsubscribe link and your mailing address. `<EmailOnly>` keeps them in the email and off the web page. A send refuses a body without them.\n\n**What comes back is always a draft.** Nothing reaches anybody until step 6 or 7. Set `slug` yourself when the article comes from a file: a slug already taken answers `422` instead of becoming something else, which is what stops a pipeline that runs twice from making two articles. It is a write, so send an `Idempotency-Key`."
      }
    },
    {
      "name": "3. Read it back",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/articles/{{article}}?expand=content",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "articles",
            "{{article}}"
          ],
          "query": [
            {
              "key": "expand",
              "value": "content"
            }
          ]
        },
        "description": "`expand=content` adds `content_markdown` to the article: the body as Commune stored it, in the same Markdown you send. What you read back is what you sent, so this is the check that nothing was lost on the way. `content` is the email rendered to HTML, with merge tags resolved against an empty context."
      }
    },
    {
      "name": "4. Change what needs changing",
      "request": {
        "method": "PATCH",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{$guid}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/articles/{{article}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "articles",
            "{{article}}"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"preview_text\": \"Three lessons from March, and the one we did not expect.\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Send only what changes. A property you leave out is left alone, and one you send as `null` is cleared. `content_markdown` replaces the whole body, so send all of it, never a fragment.\n\nA draft, a scheduled article and one whose send failed can be edited. One that is sending, sent, archived or imported answers `422`, and so does an empty object. Editing never moves the article's state."
      }
    },
    {
      "name": "5. Send yourself a test",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{$guid}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/articles/{{article}}/test-send",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "articles",
            "{{article}}",
            "test-send"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "With no addresses, the copy goes to you: the person the key belongs to. Your address is not repeated in the answer, which says `sent_to_owner: true` instead. Name up to five addresses in `to` to send it to someone else. The copy is rendered the way the real send renders it, with example data in the merge tags, a subject marked as a test and an unsubscribe link that cannot unsubscribe anybody. Nobody on the list receives anything and the article does not change, so test as often as you like. Tests do count towards the daily sending allowance.\n\nA test only needs a verified sending address and a body. It does not check the footer or the images, so it is exactly where to look for a footer that went missing before the real send refuses it."
      }
    },
    {
      "name": "6. Schedule it",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{$guid}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/articles/{{article}}/schedule",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "articles",
            "{{article}}",
            "schedule"
          ]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"scheduled_for\": \"2026-10-01T09:00:00Z\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "description": "Give it a time in the future and it is queued. `status` becomes `scheduled`, `posted_at` stays `null`, and it stays invisible to readers until it goes out. Commune sends queued articles in passes, so the time is honoured to within a few minutes rather than to the second. Scheduling it again moves it; taking it off the schedule is `POST /articles/{article}/unschedule`.\n\nEvery check the real send makes happens now, not at nine in the morning. A body without its unsubscribe link or address answers `422` (`missing_footer`), and so does an image that definitely will not load (`broken_images`), naming the URLs. Fix the image, or set `acknowledge_broken_images` to go ahead anyway; any later edit to the body clears that acknowledgement. There is no way past the footer check."
      }
    },
    {
      "name": "7. Or send it now",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{$guid}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/articles/{{article}}/send",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "articles",
            "{{article}}",
            "send"
          ]
        },
        "description": "Instead of step 6, when it should go out straight away. It cannot be undone. The answer is `202`: the article is queued for immediate dispatch, with `status` `scheduled` and `scheduled_for` set to the moment it was queued, and sending begins within a few minutes. The same checks apply as for a schedule, and an article that is already sending or sent answers `422`.\n\nThe body is optional and usually left out. Its only field is `acknowledge_broken_images`."
      }
    },
    {
      "name": "8. Confirm it went out",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/newsletters/{{newsletter}}/sends?article=8f1b7c44-2a19-4d90-b7e2-51d6c3a8f012",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "newsletters",
            "{{newsletter}}",
            "sends"
          ],
          "query": [
            {
              "key": "article",
              "value": "8f1b7c44-2a19-4d90-b7e2-51d6c3a8f012"
            }
          ]
        },
        "description": "A send is one run of the dispatch, and `article` narrows the list to this article's runs (by `id`; the short id is not accepted here). `completed_at` is `null` while recipients are still being handed over; once it is set, `recipient_count`, `sent_count` and `failed_count` are the numbers the run finished on.\n\nA recipient the provider refused for a moment is retried during the send. One that still fails is followed up by Commune rather than sent again blindly, because a second copy is worse than a late one."
      }
    },
    {
      "name": "9. Read how it did",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Commune-Version",
            "value": "{{version}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/articles/{{article}}/stats",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "articles",
            "{{article}}",
            "stats"
          ]
        },
        "description": "Both sides in one report. `email` counts recipients, not events: how many were delivered to, opened at least once, clicked at least once, bounced and unsubscribed from this article. Divide by `delivered` for the rates a provider quotes. `community` is what readers did with it on Commune: views, likes, saves, highlights and the discussion under it. It is counted when you ask and keeps rising after the send, so read it again in a week."
      }
    }
  ]
}
