Hermoso AI Posts API

The posts API from Hermoso AI — 2 operation(s) for posts.

GitHub OpenAPI

Operations 5

POST /v1/posts Create a post #
GET /v1/posts List posts #
GET /v1/posts/{id} Retrieve a post #
PATCH /v1/posts/{id} Update a scheduled post #
DELETE /v1/posts/{id} Cancel a scheduled post #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/hermoso-ai:hermoso-ai-posts-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

hermoso-ai-posts-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Hermoso Ai Posts API
  version: '2026-08-26'
  description: 'Operations tagged posts across 2 of this provider''s published API definitions: app_hermoso_ai.json, app_hermoso_ai_rest.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://app.hermoso.ai
security:
- bearerAuth: []
tags:
- name: Posts
paths:
  /v1/posts:
    post:
      operationId: createPost
      summary: Create a post
      description: 'Publish now, or schedule for later. Omit `scheduled_at` to send immediately; pass an ISO-8601 timestamp to queue it. A post fans out across every channel in `channels` INDEPENDENTLY — one channel failing does not stop the others, and `results[]` reports each. Everything that can be refused is refused HERE, while you still hold the request: an unknown channel, a visibility the channel cannot honour, a caption over a channel limit, a carousel a channel cannot carry. Nothing is ever silently downgraded at publish time.'
      tags:
      - Posts
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          type: string
        description: 'Send a unique value to make this call safe to retry. A replay returns the original response with `Idempotency-Replayed: true`, for 24 hours — except a call that queued a job, whose replay is that job as it is now (what `GET /v1/jobs/{id}` answers), so a finished render replays as done with its file.'
      - name: X-Hermoso-User
        in: header
        required: false
        schema:
          type: string
        description: 'Which profile to act on, when the account holds more than one: an `id` from `GET /v1/brands`. Defaults to the workspace the key is pinned to. Membership is re-authorised on every request, and an id this key cannot act on is a 404 `brand_not_found`, never an empty result.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostCreate'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing, invalid or revoked key — or a channel that is not connected, in which case `error.connector` names it and `error.type` is `connector_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`error.code: brand_not_found`: `X-Hermoso-User` (or the profile this key is pinned to) names a profile this key cannot act on. `GET /v1/brands` lists the valid ids.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited. `Retry-After` says how long to wait.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
      - bearerAuth: []
    get:
      operationId: listPosts
      summary: List posts
      description: Scheduled and already-fired posts for this workspace, newest first. Cursor-paginated.
      tags:
      - Posts
      parameters:
      - name: limit
        in: query
        required: false
        schema:
          type: integer
        description: 1-100, default 25.
      - name: starting_after
        in: query
        required: false
        schema:
          type: string
        description: A post id from a previous page; returns the page after it. An unknown cursor is an error, never a silent restart.
      - name: status
        in: query
        required: false
        schema:
          type: string
        description: 'Filter: scheduled, processing, published, partially_published, failed, indeterminate. `indeterminate` means a restart interrupted the send and we cannot say whether it published.'
      - name: channel
        in: query
        required: false
        schema:
          type: string
        description: Filter to posts that include this channel.
      - name: X-Hermoso-User
        in: header
        required: false
        schema:
          type: string
        description: 'Which profile to act on, when the account holds more than one: an `id` from `GET /v1/brands`. Defaults to the workspace the key is pinned to. Membership is re-authorised on every request, and an id this key cannot act on is a 404 `brand_not_found`, never an empty result.'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostList'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing, invalid or revoked key — or a channel that is not connected, in which case `error.connector` names it and `error.type` is `connector_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`error.code: brand_not_found`: `X-Hermoso-User` (or the profile this key is pinned to) names a profile this key cannot act on. `GET /v1/brands` lists the valid ids.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited. `Retry-After` says how long to wait.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
      - bearerAuth: []
    servers:
    - url: https://app.hermoso.ai
  /v1/posts/{id}:
    get:
      operationId: getPost
      summary: Retrieve a post
      description: One post, including per-channel results once it has fired.
      tags:
      - Posts
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: X-Hermoso-User
        in: header
        required: false
        schema:
          type: string
        description: 'Which profile to act on, when the account holds more than one: an `id` from `GET /v1/brands`. Defaults to the workspace the key is pinned to. Membership is re-authorised on every request, and an id this key cannot act on is a 404 `brand_not_found`, never an empty result.'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing, invalid or revoked key — or a channel that is not connected, in which case `error.connector` names it and `error.type` is `connector_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`error.code: brand_not_found`: `X-Hermoso-User` (or the profile this key is pinned to) names a profile this key cannot act on. `GET /v1/brands` lists the valid ids.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited. `Retry-After` says how long to wait.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
      - bearerAuth: []
    patch:
      operationId: updatePost
      summary: Update a scheduled post
      description: Only while it is still `scheduled`. An omitted field is left alone; an explicit empty value clears it. The edit re-runs the SAME validation a create does — it is not a looser path.
      tags:
      - Posts
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: X-Hermoso-User
        in: header
        required: false
        schema:
          type: string
        description: 'Which profile to act on, when the account holds more than one: an `id` from `GET /v1/brands`. Defaults to the workspace the key is pinned to. Membership is re-authorised on every request, and an id this key cannot act on is a 404 `brand_not_found`, never an empty result.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostUpdate'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing, invalid or revoked key — or a channel that is not connected, in which case `error.connector` names it and `error.type` is `connector_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`error.code: brand_not_found`: `X-Hermoso-User` (or the profile this key is pinned to) names a profile this key cannot act on. `GET /v1/brands` lists the valid ids.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited. `Retry-After` says how long to wait.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
      - bearerAuth: []
    delete:
      operationId: deletePost
      summary: Cancel a scheduled post
      description: Only while it is still `scheduled`. A post that has already fired cannot be cancelled — it exists on the channel now, and removing it there is that channel’s own delete.
      tags:
      - Posts
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: X-Hermoso-User
        in: header
        required: false
        schema:
          type: string
        description: 'Which profile to act on, when the account holds more than one: an `id` from `GET /v1/brands`. Defaults to the workspace the key is pinned to. Membership is re-authorised on every request, and an id this key cannot act on is a 404 `brand_not_found`, never an empty result.'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Deleted'
        '400':
          description: Invalid request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing, invalid or revoked key — or a channel that is not connected, in which case `error.connector` names it and `error.type` is `connector_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`error.code: brand_not_found`: `X-Hermoso-User` (or the profile this key is pinned to) names a profile this key cannot act on. `GET /v1/brands` lists the valid ids.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited. `Retry-After` says how long to wait.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
      - bearerAuth: []
    servers:
    - url: https://app.hermoso.ai
components:
  schemas:
    PostUpdate:
      type: object
      description: Any subset of PostCreate. Omitted means unchanged; an explicit empty value clears.
      additionalProperties: true
    PostList:
      type: object
      properties:
        object:
          type: string
          enum:
          - list
        data:
          type: array
          items:
            $ref: '#/components/schemas/Post'
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
    PostCreate:
      type: object
      required:
      - channels
      properties:
        channels:
          type: array
          items:
            type: string
          description: One or more channel ids from GET /v1/channels. DELIBERATELY NOT AN ENUM — the set is derived from what the product can publish to, and it GROWS as platform approvals land. Read it from GET /v1/channels rather than hard-coding it, and treat an id you have not seen as valid.
        caption:
          type: string
          description: The post text. Per-channel length limits are enforced at create time, not discovered at publish time.
        title:
          type: string
          description: The headline Pinterest and YouTube each require. Derived from the caption when omitted.
        link:
          type: string
        media:
          type: array
          description: Ordered. One item is a single post; two or more are a carousel, in the order given. Each item is a Hermoso-hosted URL from POST /v1/media, as a bare string or as {url, type}.
          items:
            oneOf:
            - type: string
            - type: object
              properties:
                url:
                  type: string
                type:
                  type: string
                  enum:
                  - image
                  - video
        scheduled_at:
          type: string
          format: date-time
          description: Omit to publish immediately. Otherwise ISO-8601, at least 60 seconds out and within the documented horizon.
        use_queue:
          type: boolean
          description: Take the next free slot in this profile’s posting schedule instead of naming a time. Mutually exclusive with scheduled_at — sending both is an error, never a silent precedence.
        timezone:
          type: string
          description: IANA zone for use_queue. Defaults to the profile’s saved zone.
        visibility:
          type: string
          description: public (default) | unlisted | private | draft. Refused at create time where a channel cannot honour it, never silently downgraded at publish time.
        visibility_by_channel:
          type: object
          additionalProperties:
            type: string
        captions_by_channel:
          type: object
          additionalProperties:
            type: string
          description: Override the caption for one channel.
        targets:
          type: object
          additionalProperties: true
          description: Which account to post to, where the connection holds more than one. Omit for the default. GET /v1/channels names the field each channel uses.
        options:
          type: object
          additionalProperties: true
          description: Per-channel publish metadata (YouTube description, tags, categoryId, madeForKids, notifySubscribers, paidProductPlacement, brandPartner and a captionTrack {url|text, language, name} uploaded right after the video; X thread parts, Instagram alt text, and so on).
        first_comment:
          type: string
          description: 'A comment the account posts on its own post right after it publishes: Facebook Pages, Instagram, and LinkedIn company Pages (LinkedIn needs its comments authorization, provider linkedin_community). Up to 2,000 characters, refused (never cut) when longer. Other channels publish the post without it and say so on their result. A comment that fails never undoes the post; its outcome is reported on the channel’s result.'
    Post:
      type: object
      properties:
        object:
          type: string
          enum:
          - post
        id:
          type: string
        status:
          type: string
          description: 'scheduled | processing | published | partially_published | failed | indeterminate. THIS SET MAY GROW — tolerate an unknown value. `indeterminate` means a restart interrupted the send and we cannot say whether it published: check the channel rather than assuming either way. `failed` is only ever used when we know nothing published.'
        scheduled_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        channels:
          type: array
          items:
            type: string
        caption:
          type: string
        title:
          type: string
        title_effective:
          type: string
          description: The headline actually going out — derived from the caption when you named none.
        link:
          type: string
        media:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
              url:
                type: string
        captions_by_channel:
          type: object
          nullable: true
          additionalProperties:
            type: string
        visibility:
          type: string
        visibility_by_channel:
          type: object
          nullable: true
          additionalProperties:
            type: string
        targets:
          type: object
          additionalProperties: true
          description: Which account this goes to, per channel (Page, board, LinkedIn Page, Business listing). Unset means the connection’s default.
        options:
          type: object
          additionalProperties: true
        results:
          type: array
          nullable: true
          description: Per-channel outcome once fired. A partial failure is normal and is reported here — the top-level status then says `partially_published`, never a flat `failed`.
          items:
            type: object
            properties:
              channel:
                type: string
              ok:
                type: boolean
              url:
                type: string
                nullable: true
              id:
                type: string
                nullable: true
              error:
                type: string
                nullable: true
              first_comment:
                type: object
                description: Present when the post carried `first_comment`. `posted` true with the comment `id`, or false with `note` saying why (`skipped` when the channel takes no first comment, or it was already posted by an earlier run). A failed comment never fails the channel.
                properties:
                  posted:
                    type: boolean
                  id:
                    type: string
                    nullable: true
                  skipped:
                    type: boolean
                  note:
                    type: string
                    nullable: true
        retry_of:
          type: string
          nullable: true
        status_detail:
          type: string
          nullable: true
          description: Present when the status alone would mislead. Set for `indeterminate`.
        content_lost:
          type: boolean
    Deleted:
      type: object
      properties:
        object:
          type: string
          enum:
          - post
        id:
          type: string
        deleted:
          type: boolean
    Error:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - type
          - code
          - message
          properties:
            type:
              type: string
              enum:
              - authentication_error
              - permission_error
              - invalid_request_error
              - connector_error
              - rate_limit_error
              - insufficient_credits
              - api_error
              description: The coarse family — branch on this. New CODES land inside an existing type wherever possible so that branch keeps working; the enum is published for code generation, and a client should still fall through gracefully on a member it does not recognise.
            code:
              type: string
              description: The specific reason. Log this. New values may appear at any time.
            message:
              type: string
            param:
              type: string
              description: The offending field, when there is one.
            connector:
              type: string
              description: Present when the reason is a channel that is not connected. Reconnect in the app — an OAuth consent screen cannot be completed headlessly.
            credits_needed:
              type: number
              description: 'On `insufficient_credits` from a recast or an edit: what the request needs.'
            balance:
              type: number
              description: 'On `insufficient_credits` from a recast or an edit: what this workspace has.'
            unknown_arguments:
              type: array
              items:
                type: object
                properties:
                  name:
                    type: string
                    description: The key that was sent.
                  did_you_mean:
                    type: string
                    nullable: true
                    description: An argument THIS tool declares that the key most likely meant, or null.
              description: 'On `invalid_arguments` from POST /v1/tools/{name}: each key the tool does not take, with the argument it most likely meant. Nothing was run.'
            upgrade_url:
              type: string
              description: 'On `real_face_needs_paid`: where to choose a paid plan.'
            alternative:
              type: string
              description: 'On `real_face_needs_paid`: the field that works on every plan instead (`person.describe`).'
            person_url:
              type: string
              description: 'On a recast with `person.describe` whose recast step was refused after the person was made: the AI person, in your Library. Send it as `person.image_url` to retry without making a new one.'
        request_id:
          type: string
          nullable: true
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: hmk_...
      description: A Hermoso API key, or an access token from the OAuth authorization-code flow (which is the same thing).
x-refined-from:
- app_hermoso_ai.json
- app_hermoso_ai_rest.json