Create an automation Run in API Explorer

Add MCP server to your AI tool

Allow AI tools and LLMs to interact with the API documentation portal through MCP.

MCP server URL

https://api-reference.usecommune.dev/mcp

Standard setup for AI tools providing an mcp.json file

mcp.json
{
  "REST API MCP server": {
    "url": "https://api-reference.usecommune.dev/mcp"
  }
}

Close
POST /newsletters/{newsletter}/automations

Writes an automation: what starts it, and what it does. Needs sending: write, and is a write.

It is created draft, and nothing it writes can email anybody. The engine only ever starts a run of an active automation, and the only way to that status is POST /automations/{automation}/activate, which runs every gate again and is the one operation in this contract charged to a daily cap of its own.

The graph arrives whole. steps is every step the automation will have, and every edge between them is written down: a null next means there is no edge and the run ends there, so a sequence wires each step to the one after it. There is no operation that adds one step: a step is only meaningful against the edges pointing at it.

The emails arrive inline, as body_markdown, converted by the same strict parser POST /newsletters/{newsletter}/articles uses. An automation email is not listed, not addressable and not reusable between steps, so there is no id to send instead.

Every gate runs before anything is stored. The graph (422 unprocessable): every edge points at a step that exists, every path ends, and no cycles. Each condition, against the closed keyword subset and against the paths its trigger can actually produce. Each email, against the unsubscribe-and-address footer the law requires and against a probe of every image in it. A body that would ship a compliance violation never reaches the table, which matters here more than it does for an article: an automation email is written once and then sent unattended for months.

Publishes no event.

Headers

  • Idempotency-Key string Required

    A value of your choosing naming the change this request is making.

    Send the same value again to retry the same request. Commune replays the answer the first attempt gave instead of making the change twice, and marks the replay with an Idempotent-Replay: true response header. Send a different value for a different change: a key reused for a request that differs in any way answers 409, because replaying an answer to a question you did not ask is a wrong answer you could not detect.

    A UUID per change is the usual choice. Remembered for 24 hours, per credential, so two credentials choosing the same value never see each other's answers.

    Required, not optional.

    Minimum length is 1, maximum length is 255.

  • Commune-Version string

    The contract version this request is written against. Every version published so far is a release date (YYYY-MM-DD), which is why the examples look like one, but the value is an opaque identifier: match it against the versions this API publishes rather than parsing it, because a future one may not be only a date. An unknown value answers 400 with invalid_version.

    Omitting the header pins the request to the version that was current when the API key was issued, so an integration keeps working when a newer version ships.

    Minimum length is 1.

Path parameters

  • newsletter string Required

    The newsletter's id (a UUID) or its handle. A handle is unique across Commune and is the identifier its public web profile uses, so it is the one to hardcode in an integration.

application/json

Body Required

  • name string Required

    How the automation is listed. Never shown to a subscriber.

    Minimum length is 1, maximum length is 200.

  • trigger string Required

    The event that starts a run.

    Every topic Commune publishes is a trigger, including the ones that are not about a subscriber. An automation on article.published can hold conditions and waits; it cannot send an email or move a tag, because there is nobody for those to act on, and saving one that tries is refused rather than left to fail once per event forever.

    GET /automation-triggers says, per topic, which properties its events carry and whether it is about a subscriber. That is what a condition can ask about.

    Values are article.liked, article.published, article.read, article.scheduled, automation.run.completed, automation.run.exited, automation.run.started, billing.subscription.updated, delivery.bounced, delivery.clicked, delivery.complained, delivery.delivered, delivery.opened, domain.verified, highlight.created, import.completed, message.created, send.completed, send.failed, sender.verified, subscriber.created, subscriber.status_changed, subscriber.tagged, subscriber.unsubscribed, thread.created, or thread.published.

  • trigger_filter object | null

    A JSON Schema document in the same condition subset, evaluated against the event's own payload to decide whether that event starts a run at all. Null for no filter, which is the default.

    The paths here are the payload's own, not the three-branch context a condition step is evaluated against, because a filter runs before there is a run to have a context.

  • reentry boolean

    Whether one subscriber may go through this automation more than once.

    False, the default, means once ever, however many times the trigger fires for them. A resubscribe never re-enters either, whatever this says: an address coming back to a list it was on before does not get a first welcome email for the second time.

    Default value is false.

  • steps array[object] Required

    Every step. The array order is the order they are drawn and read back in, not the order they run: what runs after a step is the step its next names, and a step whose next is null ends the run.

    At least 1 but not more than 50 elements.

    Hide steps attributes Show steps attributes object

    One step of the graph.

    Only the fields your type uses. A wait takes duration, a tag step takes tag, a condition takes schema with then and else, and a send_email takes email. Sending a field your type does not use is refused rather than ignored, because a field quietly dropped is something you believe you wrote and the engine will never see. null is accepted wherever a field is not used, so a body built by echoing a response back works unchanged.

    This is one schema rather than a union of five, deliberately. A body that fails a five-branch union reports five failures and names none of them as the branch you meant; the refusal here names the one rule that was broken.

    Every edge is written down. A null next means there is no edge, so the run ends at that step; a sequence sets next on each step to name the one after it. It used to default to the following entry in the array, which is the rule removed on 2026-10-14; see Removals under Versioning.

    • id string Required

      You choose it, and it is stable. An edge points at it and a waiting run sits on it, so changing it is removing one step and adding another: whatever was waiting on the old id has to be told where to go.

      Maximum length is 100. Format should match the following pattern: ^[a-z0-9][a-z0-9_-]*$.

    • type string Required

      What a step does.

      send_email sends the email it carries. wait parks the run for a duration. add_tag and remove_tag move a tag on the subscriber the run is about. condition evaluates a schema and takes then or else.

      There is no type for ending a run, and that is not an omission. A step with no next ends the run, as does a condition arm left null, so an ending is a field you leave out rather than a node you add. stop was such a type and was removed on 2026-10-15; see Removals under Versioning.

      Values are send_email, wait, add_tag, remove_tag, or condition.

    • next string | null

      The step to go to next, or null for no edge at all, which ends the run there. It does NOT mean "the following step in the array": that defaulting was removed on 2026-10-14, and Removals under Versioning says what a graph relying on it now does.

    • duration string | null

      wait only. An ISO 8601 duration: PT30M, PT6H, P3D, P2W. Durations only, and never a time of day: Commune does not know a subscriber's time zone, so a time of day would be UTC and would land in the middle of the night for much of any real audience. At most 730 days.

    • tag string(uuid) | null

      add_tag and remove_tag only. The id of one of this newsletter's tags. A tag that has been deleted is refused: a step naming one would do nothing, every run, forever.

    • schema object | null

      condition only. A JSON Schema document in Commune's condition subset, evaluated against the run's context to choose between then and else.

      The subset is closed: type, const, enum, properties, required, contains, minimum, maximum, minLength, maxLength, minItems, maxItems, allOf, anyOf, oneOf and not. Anything else is refused rather than ignored, which is the inversion that matters. A general validator ignores a keyword it does not implement, so an unsupported keyword becomes a constraint you wrote and nothing enforces.

      Three refusals are worth knowing the reason for. pattern and patternProperties would run your regular expression against text a subscriber controls. $ref and $id can be a remote fetch, and this runs in a server loop. if/then/else collide with this step's own branch fields, and anyOf and not already cover them.

      Paths are checked too. A condition about a property no payload carries matches every run, so the branch would always take the same arm; such a path is refused at save time, naming it. GET /automation-triggers lists what each trigger's context carries.

    • then string | null

      condition only. The step to go to when the schema matches, or null to end the run on that arm. It used to fall through to the following step, so an arm left null ran into whatever came after the condition; see Removals under Versioning.

    • else string | null

      condition only. The step to go to when the schema does not match, or null to end the run on that arm. Leaving one arm null is how a condition means "do this, or nothing".

    • email object | null

      The email a send_email step sends, inline on the step.

      Inline rather than by id, because an automation email has no life of its own: it is not listed, not addressable and not reusable between steps, so an id would be an identifier for something you can never hold.

      The body is Markdown and the parser is strict. A body it cannot read is a 400 naming the line, and nothing is written. That matters more here than for an article: an article is looked at before it goes out, and an automation email is written once and then sent unattended for months, so a guess becomes a month of mail nobody checked.

      Two gates run on this body before it is stored, and again when the automation is turned on. It has to carry an unsubscribe link and a mailing address, which Commune seeds into every draft as ordinary content and which can therefore be written away. And every image in it has to load: an inbox fetches images fresh when the reader opens the email, so one that has rotted is broken for every recipient at once and cannot be repaired afterwards.

      Additional properties are NOT allowed.

      Hide email attributes Show email attributes object | null
      • subject string Required

        The subject line. Personalization placeholders work here as they do in the body, so {{ subscriber.first_name }} is resolved per recipient.

        Minimum length is 1, maximum length is 500.

      • preview_text string | null

        The line an inbox shows after the subject, or null for none.

        Maximum length is 500.

      • body_markdown string Required

        The email, as Markdown. Commune's own components are written in an MDX-shaped syntax; an unrecognised one is refused rather than passed through as text.

        Maximum length is 500000.

Responses

  • 201 application/json

    The automation, as a draft, with the graph it was given.

    Hide response attributes Show response attributes object
    • object string Required

      Always automation.

      Value is automation.

    • id string(uuid) Required
    • newsletter object | null Required

      The newsletter this automation belongs to. A Ref unless newsletter is named in ?expand=.

      One of:
    • name string Required
    • status string Required

      Where an automation is in its life.

      draft is where every automation starts and the only status a create can produce. active is the one status that sends: the engine's ingest and its claim on due work both filter on it, so nothing an automation holds reaches anybody until somebody turns it on.

      paused holds every waiting run exactly where it is. A run three days into a sequence stays on that step and resumes from it when the automation is turned on again, so a pause is instant and total rather than a drain.

      archived is what DELETE leaves behind. It is not sending and it is not coming back: the runs it was in the middle of are ended, because a run holding its place on a graph nobody will activate again is a subscriber stuck part way through forever. The automation and its history stay readable, which is the reason it is archived rather than deleted.

      Values are draft, active, paused, or archived.

    • trigger string Required

      The event that starts a run.

      Every topic Commune publishes is a trigger, including the ones that are not about a subscriber. An automation on article.published can hold conditions and waits; it cannot send an email or move a tag, because there is nobody for those to act on, and saving one that tries is refused rather than left to fail once per event forever.

      GET /automation-triggers says, per topic, which properties its events carry and whether it is about a subscriber. That is what a condition can ask about.

      Values are article.liked, article.published, article.read, article.scheduled, automation.run.completed, automation.run.exited, automation.run.started, billing.subscription.updated, delivery.bounced, delivery.clicked, delivery.complained, delivery.delivered, delivery.opened, domain.verified, highlight.created, import.completed, message.created, send.completed, send.failed, sender.verified, subscriber.created, subscriber.status_changed, subscriber.tagged, subscriber.unsubscribed, thread.created, or thread.published.

    • trigger_filter object | null Required
    • reentry boolean Required
    • steps array[object] Required

      The graph. The order is the order the steps are drawn and read back in, not the order they run: follow next, then and else for that.

      Hide steps attributes Show steps attributes object

      One step of the graph.

      Every field is present on every step, null where the type does not use it. That is the opposite of the request shape, which refuses a field a type does not use, and the asymmetry is deliberate: a reader wants one key set to narrow, and a writer wants to be told when it has said something meaningless. A body built by sending this back is accepted unchanged.

      • id string Required
      • type string Required

        What a step does.

        send_email sends the email it carries. wait parks the run for a duration. add_tag and remove_tag move a tag on the subscriber the run is about. condition evaluates a schema and takes then or else.

        There is no type for ending a run, and that is not an omission. A step with no next ends the run, as does a condition arm left null, so an ending is a field you leave out rather than a node you add. stop was such a type and was removed on 2026-10-15; see Removals under Versioning.

        Values are send_email, wait, add_tag, remove_tag, or condition.

      • next string | null Required

        The step this one goes to, or null when there is no edge, which ends the run there. Null used to mean "the next step in order", and was the common case because a linear sequence wired nothing; see Removals under Versioning.

      • then string | null Required
      • else string | null Required
      • duration string | null Required
      • tag string(uuid) | null Required
      • schema object | null Required
      • email object | null

        The email a send_email step sends, as it is stored.

        body_markdown is the same rendition you sent, so reading an automation, changing one step and sending the graph back is a round trip that preserves everything else. Personalization placeholders come back as you wrote them for that reason: resolving them here would silently replace every greeting in the automation with its fallback the next time somebody saved it.

        Additional properties are NOT allowed.

        Hide email attributes Show email attributes object | null
        • subject string Required
        • preview_text string | null Required
        • body_markdown string | null Required

          The body as Markdown, or null for a stored document this API cannot render as Markdown faithfully.

    • created_at string(date-time) Required
    • updated_at string(date-time) Required
    • activated_at string(date-time) | null Required

      When this automation was last turned on, or null if it never has been. It is not cleared by pausing, so it answers "has this ever sent anything".

  • 400 application/json

    The request was malformed, and the same request will fail the same way until it is changed. param names the parameter or header at fault when there is exactly one, and allowed_values lists what it accepts when that is a finite set. The code is bad_request for every case below except the last.

    • A query parameter: one the operation does not have, a value outside its set, range or format (an unparseable cursor, an unknown expand path or fields name, an identifier that is not a UUID), or a required one left out, such as q on a search or newsletter when the credential reaches more than one.
    • The request body: not JSON, not the shape the operation reads, a property it does not write, or a value of the wrong type, length or format. param is absent here, since the body is not a parameter, and the message names the property.
    • The Idempotency-Key header, on an operation that changes something: missing, or a value this API will not store.
    • An unrecognised Commune-Version, which answers with its own code, invalid_version, because it is never fixed by changing the body.
    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

  • 401 application/json

    No credential was presented, or it is malformed, unknown, revoked or expired, or it is an access token minted for a different audience.

    Every one of these answers identically, down to the wording and the headers, so a refusal never confirms that a string was once real.

    Hide headers attribute Show headers attribute
    • WWW-Authenticate string

      The authentication scheme this API accepts, and where to find out how to get a credential for it. Always Bearer realm="Commune API", resource_metadata="https://api.usecommune.com/.well-known/oauth-protected-resource".

      resource_metadata is the RFC 9728 pointer to this API's protected resource metadata, which names the authorization server an OAuth client should send its user to. A client holding an API key can ignore it. The header carries no error parameter, not even error="invalid_token", because it describes what this API accepts rather than what was wrong with the credential sent, and the reasons above are deliberately indistinguishable.

      There is no second scheme and no query-parameter fallback, because a credential that can travel in a URL ends up in access logs and referer headers.

    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

  • 402 application/json

    The credential is allowed to do this but the newsletter's plan does not include it.

    Two surfaces can answer it: insights, the engagement and metrics operations, which are the only reads Commune reserves the right to meter, and writing, every operation that changes something.

    Every other read stays free on every plan, so a credential refused at one of these can still read everything else. The body names the plan the newsletter is on and the plans that would work.

    One refusal is a feature of the paid plan rather than a charge for API access, and it answers whatever is metered: a newsletter whose plan does not include it may have one automation active at a time, so POST /automations/{automation}/activate is refused while another is running. That one is also the only 402 a client can clear on its own, by pausing the automation that is running.

    This status is predictable and should not be how you discover it. GET /newsletters/{newsletter}/entitlements answers the same question in advance, carrying the same plan list this puts in allowed_values and the same sentence it puts in message. Read it once at the start of a run rather than finding out in the middle of one.

    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

  • 403 application/json

    The credential is valid but is not allowed to do this. Two codes answer with this status, and error.code says which.

    insufficient_scope: it does not hold the permission. The operation needs, say, audience: read on the newsletter addressed, and this credential holds less than that there. allowed_values carries the permission that was needed, and the message says what the credential does hold on that newsletter, because a credential granted the wrong family and a credential belonging to somebody whose standing on the team has narrowed look identical without it. The answer can differ per newsletter: the same credential may be allowed here and refused on the next one it reaches.

    The same code answers an operation that needs the account permission from a credential that does not carry it. That permission is about the person a credential belongs to rather than about any newsletter, so nothing granted on a newsletter adds up to it. It is granted on the credential itself, when a key is minted or when an authorization asks for account:read.

    And it answers a parameter the credential may send, but not with the value it sent: a filter a credential holding only read permissions may not use, or an expand path whose rows need a permission the operation does not. param names the parameter, and allowed_values carries what this credential may send instead, or is absent when it may send nothing there at all.

    forbidden: it may not act here at all. Either the credential does not reach the newsletter addressed, because it was never granted it or because the person it belongs to can no longer act on it, or it reaches no newsletter at all; param is newsletter, and GET /newsletters lists the ones it does reach. Or, on DELETE /api-keys/{key}, the credential named belongs to somebody else. Neither carries allowed_values, because there is no value to send instead.

    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

  • 404 application/json

    No such resource, or the key is not allowed to know that it exists. Commune answers 404 rather than 403 where distinguishing the two would leak the existence of private content.

    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

  • 409 application/json

    The request collided with something. On a write this is always the Idempotency-Key, in one of two ways, and the message says which.

    Either the key was already used for a different request, which is refused rather than answered with the earlier request's result. Or an earlier request using the same key has not finished, or never reported an outcome, in which case this one was not run and the key becomes usable again shortly.

    Nothing was changed by a request that answers this.

    Hide headers attribute Show headers attribute
    • Retry-After integer

      Seconds to wait before retrying, on the second case only.

      Minimum value is 1.

    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

  • 422 application/json

    The request is well formed and every value in it is legal, and the state of what it addresses refuses it anyway. The message says what about that state is in the way.

    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

  • 429 application/json

    Too many requests. Back off and retry after the interval named by the Retry-After response header.

    One of the budgets in RateLimit-Policy ran out, and the RateLimit-* headers on this response say which and when it resets.

    Hide headers attribute Show headers attribute
    • Retry-After integer

      Seconds to wait before retrying.

      Minimum value is 1.

    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

  • 500 application/json

    Something failed inside Commune. The request may be retried.

    Hide response attribute Show response attribute object
    • error object Required

      Additional properties are NOT allowed.

      Hide error attributes Show error attributes object
      • code string Required

        The stable, machine readable reason a request failed. New codes may be added in a minor version, so treat an unrecognised code as a generic failure of its HTTP status class.

        Two of these share a status with a neighbour and exist because what a caller does next is different. invalid_version is a 400 that is never fixed by changing the request body. not_commune_newsletter is a 422 that is never fixed by changing the request at all: it says the newsletter's articles are published somewhere else and mirrored into Commune afterwards, so Commune cannot write one. Its page at https://usecommune.dev/errors/not_commune_newsletter, like every code's, is its docs_url, and it covers moving a newsletter onto Commune's own publishing, which is the only thing that resolves it.

        Values are bad_request, invalid_version, unauthorized, forbidden, insufficient_scope, payment_required, not_found, conflict, unprocessable, not_commune_newsletter, rate_limited, internal_error, or service_unavailable.

      • message string Required

        A human readable sentence describing what went wrong. Written for a developer reading a log, not for an end user. Do not branch on it, branch on code.

      • param string

        The query, path or body parameter the error is attributed to, when the error is attributable to exactly one. Absent otherwise.

      • allowed_values array[string]

        Everything param would have accepted, when what it accepts is a finite set. Absent when it is not: a cursor, an identifier or a numeric range has nothing to enumerate, and an empty array would read as "nothing is allowed".

        It repeats what message says in prose, so a caller can correct a request from this one response: the array is what a program branches on, the sentence is what a person or a model reads.

        On an unknown parameter name rather than an unknown value, this carries the parameter names the operation does accept, since that is the set the caller has to pick from.

        On an insufficient_scope failure there is usually no parameter at fault and param is absent, and this carries the one permission that was needed, written the way the permission table writes it, such as content: read. The exception is a credential that may call the operation but not with one value of a parameter, such as ?expand=subscriber on listNewsletterInsights without audience: read: then param names the parameter and this carries the values this credential may send instead.

      • request_id string

        Identifier for this request, echoed in the Commune-Request-Id response header. Quote it in support requests.

      • docs_url string(uri)

        Link to the documentation for this error code: always https://usecommune.dev/errors/ followed by the code, a page on what the code means, what usually causes it and how to fix it.

POST /newsletters/{newsletter}/automations
curl \
 --request POST 'https://api.usecommune.com/newsletters/9a4c1c6e-0f2b-4f47-9d3f-6d1b1a2c3d4e/automations' \
 --header "Authorization: Bearer $ACCESS_TOKEN" \
 --header "Content-Type: application/json" \
 --header "Idempotency-Key: 3f7c1a26-9b0e-4f5a-9a2c-2c8f1d6b4e77" \
 --header "Commune-Version: 2026-08-26" \
 --data '{
  "name": "Welcome sequence",
  "trigger": "subscriber.created",
  "reentry": false,
  "steps": [
    {
      "id": "welcome",
      "type": "send_email",
      "email": {
        "subject": "Welcome, {{ subscriber.first_name }}",
        "body_markdown": "Glad you are here.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "settle",
      "type": "wait",
      "duration": "P3D"
    },
    {
      "id": "best-of",
      "type": "send_email",
      "email": {
        "subject": "Three things worth reading",
        "body_markdown": "Here is where to start.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ]
}'
Request examples
# Headers
Idempotency-Key: 3f7c1a26-9b0e-4f5a-9a2c-2c8f1d6b4e77
Commune-Version: 2026-08-26

# Payload
{
  "name": "Welcome sequence",
  "trigger": "subscriber.created",
  "reentry": false,
  "steps": [
    {
      "id": "welcome",
      "type": "send_email",
      "email": {
        "subject": "Welcome, {{ subscriber.first_name }}",
        "body_markdown": "Glad you are here.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    },
    {
      "id": "settle",
      "type": "wait",
      "duration": "P3D"
    },
    {
      "id": "best-of",
      "type": "send_email",
      "email": {
        "subject": "Three things worth reading",
        "body_markdown": "Here is where to start.\n\n[Unsubscribe]({{ unsubscribe_url }}) · {{ address }}\n"
      }
    }
  ]
}
Response examples (201)
{
  "object": "automation",
  "id": "string",
  "newsletter": {
    "object": "newsletter",
    "id": "9a4c1c6e-0f2b-4f47-9d3f-6d1b1a2c3d4e"
  },
  "name": "string",
  "status": "draft",
  "trigger": "article.liked",
  "trigger_filter": {},
  "reentry": true,
  "steps": [
    {
      "id": "string",
      "type": "send_email",
      "next": "string",
      "then": "string",
      "else": "string",
      "duration": "string",
      "tag": "string",
      "schema": {},
      "email": {
        "subject": "string",
        "preview_text": "string",
        "body_markdown": "string"
      }
    }
  ],
  "created_at": "2026-05-04T09:42:00Z",
  "updated_at": "2026-05-04T09:42:00Z",
  "activated_at": "2026-05-04T09:42:00Z"
}
Response examples (400)
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}
Response examples (401)
# Headers
WWW-Authenticate: string

# Payload
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}
Response examples (402)
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}
Response examples (403)
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}
Response examples (404)
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}
Response examples (409)
# Headers
Retry-After: 42

# Payload
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}
Response examples (422)
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}
Response examples (429)
# Headers
Retry-After: 42

# Payload
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}
Response examples (500)
{
  "error": {
    "code": "bad_request",
    "message": "Newsletter not found.",
    "param": "cursor",
    "allowed_values": [
      "subscribed",
      "unsubscribed",
      "bounced",
      "complained",
      "pending"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/not_found"
  }
}