{
    "info": {
        "name": "Publicasta APIs",
        "description": "Read published content without authentication through the Public Content API, or create channels and articles through the authenticated Publishing API. Successful publishing requests can return moderation_review first; call Get article until the final translation statuses are clear.",
        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
    },
    "event": [
        {
            "listen": "prerequest",
            "script": {
                "type": "text/javascript",
                "exec": [
                    "const requestId = 'postman-' + Date.now() + '-' + pm.variables.replaceIn('{{$randomUUID}}');",
                    "pm.request.headers.upsert({ key: 'X-Request-ID', value: requestId });"
                ]
            }
        }
    ],
    "variable": [
        {
            "key": "base_url",
            "value": "https://publicasta.com",
            "type": "string"
        },
        {
            "key": "channel_slug",
            "value": "demo_project",
            "type": "string"
        },
        {
            "key": "api_key",
            "value": "PASTE_API_KEY_HERE",
            "type": "secret"
        },
        {
            "key": "image_path",
            "value": "/absolute/path/to/illustration.png",
            "type": "string"
        },
        {
            "key": "image_url",
            "value": "PASTE_UPLOADED_IMAGE_URL_HERE",
            "type": "string"
        },
        {
            "key": "article_slug",
            "value": "agent_post",
            "type": "string"
        },
        {
            "key": "scheduled_at",
            "value": "2026-05-20T10:00:00+00:00",
            "type": "string"
        },
        {
            "key": "article_language",
            "value": "en",
            "type": "string"
        },
        {
            "key": "search_query",
            "value": "multilingual publishing",
            "type": "string"
        }
    ],
    "item": [
        {
            "name": "Public reading (no API key)",
            "description": "Discover, search, and read only published Publicasta content. These requests require no authentication.",
            "item": [
                {
                    "name": "Machine discovery",
                    "request": {
                        "method": "GET",
                        "auth": {
                            "type": "noauth"
                        },
                        "header": [
                            {
                                "key": "Accept",
                                "value": "text/markdown"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/llms.txt",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "llms.txt"
                            ]
                        },
                        "description": "Lists the public reading entry points, URL patterns, language behavior, and citation fields."
                    }
                },
                {
                    "name": "List public channels",
                    "request": {
                        "method": "GET",
                        "auth": {
                            "type": "noauth"
                        },
                        "header": [
                            {
                                "key": "Accept",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/public/v1/channels?limit=20",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "public",
                                "v1",
                                "channels"
                            ],
                            "query": [
                                {
                                    "key": "limit",
                                    "value": "20"
                                }
                            ]
                        },
                        "description": "Returns public channels with opaque cursor pagination. Follow _links.next instead of constructing cursors."
                    }
                },
                {
                    "name": "Search published articles",
                    "request": {
                        "method": "GET",
                        "auth": {
                            "type": "noauth"
                        },
                        "header": [
                            {
                                "key": "Accept",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/public/v1/search?q={{search_query}}&language={{article_language}}&limit=20",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "public",
                                "v1",
                                "search"
                            ],
                            "query": [
                                {
                                    "key": "q",
                                    "value": "{{search_query}}"
                                },
                                {
                                    "key": "language",
                                    "value": "{{article_language}}"
                                },
                                {
                                    "key": "limit",
                                    "value": "20"
                                }
                            ]
                        },
                        "description": "Searches published translations. Add channel={{channel_slug}} to restrict results to one channel."
                    }
                },
                {
                    "name": "Read article as JSON",
                    "request": {
                        "method": "GET",
                        "auth": {
                            "type": "noauth"
                        },
                        "header": [
                            {
                                "key": "Accept",
                                "value": "application/json"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/public/v1/channels/{{channel_slug}}/articles/{{article_slug}}?lang={{article_language}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "public",
                                "v1",
                                "channels",
                                "{{channel_slug}}",
                                "articles",
                                "{{article_slug}}"
                            ],
                            "query": [
                                {
                                    "key": "lang",
                                    "value": "{{article_language}}"
                                }
                            ]
                        },
                        "description": "Returns metadata, full content_markdown, published translations, canonical URLs, and links to every reading surface."
                    }
                },
                {
                    "name": "Read article as Markdown",
                    "request": {
                        "method": "GET",
                        "auth": {
                            "type": "noauth"
                        },
                        "header": [
                            {
                                "key": "Accept",
                                "value": "text/markdown"
                            }
                        ],
                        "url": {
                            "raw": "{{base_url}}/api/public/v1/channels/{{channel_slug}}/articles/{{article_slug}}.md?lang={{article_language}}",
                            "host": [
                                "{{base_url}}"
                            ],
                            "path": [
                                "api",
                                "public",
                                "v1",
                                "channels",
                                "{{channel_slug}}",
                                "articles",
                                "{{article_slug}}.md"
                            ],
                            "query": [
                                {
                                    "key": "lang",
                                    "value": "{{article_language}}"
                                }
                            ]
                        },
                        "description": "Returns the full article as Markdown with self-describing YAML front matter."
                    }
                }
            ]
        },
        {
            "name": "List channels",
            "request": {
                "method": "GET",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels"
                    ]
                },
                "description": "Returns owned and shared channels available to the API key. Inspect owner, can_write, can_manage_settings, and read_only before choosing an operation."
            }
        },
        {
            "name": "Create channel",
            "request": {
                "method": "POST",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    },
                    {
                        "key": "Content-Type",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels"
                    ]
                },
                "body": {
                    "mode": "raw",
                    "raw": "{\n    \"slug\": \"{{channel_slug}}\",\n    \"default_language\": \"en\",\n    \"status\": \"published\",\n    \"translations\": [\n        {\n            \"language\": \"en\",\n            \"title\": \"Agent channel\",\n            \"description\": \"A publishing channel prepared through the Publicasta API.\"\n        }\n    ]\n}",
                    "options": {
                        "raw": {
                            "language": "json"
                        }
                    }
                },
                "description": "Creates a channel owned by the API key owner. This operation uses that account's own API entitlement and channel limit."
            },
            "event": [
                {
                    "listen": "test",
                    "script": {
                        "type": "text/javascript",
                        "exec": [
                            "pm.test('Channel created or validation returned a clear response', function () {",
                            "  pm.expect(pm.response.code).to.be.oneOf([201, 422, 403]);",
                            "});"
                        ]
                    }
                }
            ]
        },
        {
            "name": "Upload channel cover",
            "request": {
                "method": "POST",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels/{{channel_slug}}/media",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels",
                        "{{channel_slug}}",
                        "media"
                    ]
                },
                "body": {
                    "mode": "formdata",
                    "formdata": [
                        {
                            "key": "purpose",
                            "value": "channel_cover",
                            "type": "text"
                        },
                        {
                            "key": "image",
                            "type": "file",
                            "src": "{{image_path}}"
                        }
                    ]
                },
                "description": "Uploads and immediately sets the channel cover image."
            }
        },
        {
            "name": "Upload channel avatar",
            "request": {
                "method": "POST",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels/{{channel_slug}}/media",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels",
                        "{{channel_slug}}",
                        "media"
                    ]
                },
                "body": {
                    "mode": "formdata",
                    "formdata": [
                        {
                            "key": "purpose",
                            "value": "channel_avatar",
                            "type": "text"
                        },
                        {
                            "key": "image",
                            "type": "file",
                            "src": "{{image_path}}"
                        }
                    ]
                },
                "description": "Uploads, crops and immediately sets the channel avatar image."
            }
        },
        {
            "name": "Upload image",
            "request": {
                "method": "POST",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels/{{channel_slug}}/media",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels",
                        "{{channel_slug}}",
                        "media"
                    ]
                },
                "body": {
                    "mode": "formdata",
                    "formdata": [
                        {
                            "key": "image",
                            "type": "file",
                            "src": "{{image_path}}"
                        },
                        {
                            "key": "alt",
                            "value": "Generated illustration",
                            "type": "text"
                        },
                        {
                            "key": "watermark",
                            "value": "auto",
                            "type": "text",
                            "description": "Optional: auto follows the channel default, on forces the configured channel watermark, and off skips it for this image."
                        }
                    ]
                },
                "description": "Set api_key, channel_slug, and image_path collection variables before sending. Copy markdown or html from the response into the article request. If page_slug is omitted, the image is stored as draft media; you may use that draft URL in every translation and Publicasta rewrites it to the final page media URL when the article is saved or published. If the article already exists and you need the final URL immediately, send page_slug. Distinct page-bound images may be uploaded first and then used in sequential translation updates. The optional watermark field accepts auto, on, or off. Existing clients can omit it: auto is the default. Inspect watermark_applied and watermark_type in the response."
            },
            "event": [
                {
                    "listen": "test",
                    "script": {
                        "type": "text/javascript",
                        "exec": [
                            "pm.test('Image uploaded', function () {",
                            "  pm.expect(pm.response.code).to.eql(201);",
                            "});",
                            "",
                            "pm.test('Response has markdown snippet', function () {",
                            "  const json = pm.response.json();",
                            "  pm.expect(json).to.have.property('markdown');",
                            "});"
                        ]
                    }
                }
            ]
        },
        {
            "name": "List articles",
            "request": {
                "method": "GET",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels/{{channel_slug}}/pages?per_page=25",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels",
                        "{{channel_slug}}",
                        "pages"
                    ],
                    "query": [
                        {
                            "key": "per_page",
                            "value": "25"
                        },
                        {
                            "key": "status",
                            "value": "draft",
                            "disabled": true
                        },
                        {
                            "key": "cursor",
                            "value": "",
                            "disabled": true
                        }
                    ]
                },
                "description": "Returns lightweight article summaries without content_html. Filter by status when needed. While pagination.has_more is true, repeat the request with pagination.next_cursor in the cursor query parameter. Use the returned slug or numeric id in all channel article routes."
            }
        },
        {
            "name": "Create article",
            "request": {
                "method": "POST",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    },
                    {
                        "key": "Content-Type",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels/{{channel_slug}}/pages",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels",
                        "{{channel_slug}}",
                        "pages"
                    ]
                },
                "body": {
                    "mode": "raw",
                    "raw": "{\n    \"slug\": \"{{article_slug}}\",\n    \"default_language\": \"en\",\n    \"status\": \"published\",\n    \"publish_translations\": true,\n    \"scheduled_at\": null,\n    \"translations\": [\n        {\n            \"language\": \"en\",\n            \"title\": \"Agent post\",\n            \"excerpt\": \"A separate teaser shown below the title and in previews.\",\n            \"content_markdown\": \"Body from **agent**.\\n\\n![Generated illustration]({{image_url}})\\n\\n## First section\\n\\nContinue the article here.\"\n        },\n        {\n            \"language\": \"ru\",\n            \"title\": \"Post agenta\",\n            \"excerpt\": \"Separate teaser for article cards and the article lead.\",\n            \"content_markdown\": \"Tekst ot **agenta**.\\n\\n## Pervyi razdel\\n\\nProdolzhaite statyu zdes.\"\n        }\n    ]\n}",
                    "options": {
                        "raw": {
                            "language": "json"
                        }
                    }
                },
                "description": "Creates or updates an article. Put the main title in translations[].title, not in content_markdown/content_html. translations[].excerpt is a separate standfirst/teaser shown below the title and in previews, with a maximum length of 320 characters; do not copy or truncate the first body paragraph into excerpt if that paragraph remains in content_markdown/content_html. Use ## and ### for body sections. Markdown direction follows translations[].language. In content_html, dir is preserved only on supported block elements with ltr, rtl, or auto; keep URLs, email, keys and code LTR and do not use Unicode bidi override controls. status can be draft or published. Publishing and scheduling enter asynchronous moderation first; a successful response can be moderation_review, not final publication. When scheduled_at is present, moderation starts immediately and publication happens later. On POST, published or scheduled multi-translation articles publish or schedule all sent translations by default; set publish_translations=false only when secondary translations must stay drafts. After the write response, call Get article and inspect translation_status_summary and translations[].status until the final state is clear. If scheduled_paused=true appears later, the article lost scheduled publishing access and needs a new time or manual publication after upgrade."
            },
            "event": [
                {
                    "listen": "test",
                    "script": {
                        "type": "text/javascript",
                        "exec": [
                            "pm.test('Request succeeded', function () {",
                            "  pm.expect(pm.response.code).to.be.oneOf([200, 201]);",
                            "});",
                            "",
                            "pm.test('Response has public URL', function () {",
                            "  const json = pm.response.json();",
                            "  pm.expect(json).to.have.property('url');",
                            "});",
                            "",
                            "pm.test('Response exposes lifecycle status', function () {",
                            "  const json = pm.response.json();",
                            "  pm.expect(json).to.have.property('status');",
                            "  pm.expect(json).to.have.property('translations');",
                            "});"
                        ]
                    }
                }
            ]
        },
        {
            "name": "Get article",
            "request": {
                "method": "GET",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels/{{channel_slug}}/pages/{{article_slug}}",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels",
                        "{{channel_slug}}",
                        "pages",
                        "{{article_slug}}"
                    ]
                },
                "description": "Returns current article status, scheduled_at, scheduled_paused, scheduled_pause_reason, published_at, translation_status_summary, translation statuses, and translation content_html. article_slug may contain either the article slug or its numeric id from List articles or a write response. Use this after every write request. If the article is moderation_review immediately after publishing or scheduling, moderation is still running asynchronously; poll again before reporting final publication."
            }
        },
        {
            "name": "Upsert one translation",
            "request": {
                "method": "PUT",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    },
                    {
                        "key": "Content-Type",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels/{{channel_slug}}/pages/{{article_slug}}/translations/es",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels",
                        "{{channel_slug}}",
                        "pages",
                        "{{article_slug}}",
                        "translations",
                        "es"
                    ]
                },
                "body": {
                    "mode": "raw",
                    "raw": "{\n    \"title\": \"Post del agente\",\n    \"excerpt\": \"Resumen corto\",\n    \"content_markdown\": \"Texto del **agente**.\\n\\n## Primera secci\\u00f3n\\n\\nContin\\u00faa el art\\u00edculo aqu\\u00ed.\"\n}",
                    "options": {
                        "raw": {
                            "language": "json"
                        }
                    }
                },
                "description": "Creates or updates a single translation without resending the default language or other translations."
            }
        },
        {
            "name": "Schedule article",
            "request": {
                "method": "PATCH",
                "header": [
                    {
                        "key": "Accept",
                        "value": "application/json"
                    },
                    {
                        "key": "Content-Type",
                        "value": "application/json"
                    }
                ],
                "auth": {
                    "type": "bearer",
                    "bearer": [
                        {
                            "key": "token",
                            "value": "{{api_key}}",
                            "type": "string"
                        }
                    ]
                },
                "url": {
                    "raw": "{{base_url}}/api/v1/channels/{{channel_slug}}/pages/{{article_slug}}/status",
                    "host": [
                        "{{base_url}}"
                    ],
                    "path": [
                        "api",
                        "v1",
                        "channels",
                        "{{channel_slug}}",
                        "pages",
                        "{{article_slug}}",
                        "status"
                    ]
                },
                "body": {
                    "mode": "raw",
                    "raw": "{\n    \"status\": \"scheduled\",\n    \"publish_translations\": true,\n    \"scheduled_at\": \"{{scheduled_at}}\"\n}",
                    "options": {
                        "raw": {
                            "language": "json"
                        }
                    }
                },
                "description": "Changes article status or delayed publication time without resending article content. scheduled_at must be ISO 8601 with an explicit timezone. For existing multilingual articles, publish_translations is required: true submits translations together with the primary language, false keeps secondary translations as drafts. Publishing and scheduling enter asynchronous moderation first; call Get article after this request to confirm whether the final state is scheduled, published, blocked, or still waiting for review. Setting a new scheduled_at clears an account-plan pause."
            }
        }
    ]
}