Hermoso AI Recasts API

The recasts API from Hermoso AI — 1 operation(s) for recasts.

GitHub OpenAPI

Operations 1

POST /v1/recasts Put a person into a video (motion transfer) #

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-recasts-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-recasts-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Hermoso Ai Recasts API
  version: '2026-08-26'
  description: 'Operations tagged recasts 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: recasts
paths:
  /v1/recasts:
    post:
      operationId: createRecast
      summary: Put a person into a video (motion transfer)
      description: 'Replace the person in a video with someone else: a saved or preset creator, a photo, or an AI person made from a description. The new person performs every move, expression and mouth shape of the original, on the same beats and cuts, in the same set and light, and the original sound is kept. Optionally give them a new voice, or have them speak another language with the mouth lip-synced.


        PRICED UP FRONT. Send `"dry_run": true` and the response is the exact price: the credits the job reserves when it starts, from the job''s own pricing. It is an estimate: the real call reserves the same figure and is charged its actual cost, usually within a few percent of it. Nothing runs and nothing is charged on a dry run, except that reading a social post link uses its one lookup.


        ASYNC. A real call answers `202` with the job inside and a `Location` to poll (`GET /v1/jobs/{id}`), usually a few minutes. Or name a `callback_url` and the finished job is POSTed to you.


        PEOPLE AND LIKENESS. A photo of a real person needs a paid plan, and sending one is your confirmation that you have that person''s permission (it is recorded). An AI person from `describe`, or a preset creator, works on every plan. A request that would be refused is refused before anything is charged, with a stable `error.code` and a plain sentence: `real_face_needs_paid`, `face_review_blocked`, `ref_unreadable`, `insufficient_credits`.'
      tags:
      - recasts
      parameters:
      - name: callback_url
        in: query
        required: false
        schema:
          type: string
        description: The same as the `X-Hermoso-Callback-Url` header or the body field, for a client that cannot set headers.
      - name: X-Hermoso-Callback-Url
        in: header
        required: false
        schema:
          type: string
          format: uri
        description: 'An https:// URL to POST the outcome to, for a platform that cannot poll (for example Zapier `z.generateCallbackUrl()`). When the call queued a job, Hermoso POSTs the job resource — exactly what `GET /v1/jobs/{id}` answers — once the job is `done` or `error`, with headers `Hermoso-Event: job.done | job.error` and `Hermoso-Delivery` (also sent as `Idempotency-Key`; drop a repeat). A 2xx ends delivery; a 4xx other than 408, 425 and 429 is final; anything else is retried for about 16 hours. When the call finished inside the request, its response body is POSTed too (`Hermoso-Event: tool.result`). The response header `Hermoso-Callback` says what happened: `registered`, `sent`, `limit-reached` or `not-registered`. Nothing about what runs or what it costs changes: the URL is attached after the job is queued. A refusal is the HTTP error and nothing is POSTed. Must be https on a public host name; a replay with the same `Idempotency-Key` and a new URL adds that URL to the same job.'
      - 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/RecastCreate'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Recast'
        '202':
          description: Queued. `job` is the job as it is now; poll `job.poll` (also sent as `Location`) until `status` is `done` (its `url` is the video) or `error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Recast'
        '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'
        '402':
          description: '`insufficient_credits`: the balance does not cover `credits_needed` (nothing was started). `real_face_needs_paid`: a photo of a real person needs a paid plan (`upgrade_url`), or use `person.describe`.'
          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'
        '422':
          description: '`child_safety_blocked`: the request would put a child into sexual or sexualised content, so nothing ran (the only request Hermoso refuses itself; the video model''s own refusals are passed through in plain words). `ref_unreadable`: a picture or link could not be opened.'
          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'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -sX POST https://app.hermoso.ai/v1/recasts \\\n  -H \"Authorization: Bearer hmk_...\" -H 'content-type: application/json' \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\n    \"video\": \"https://www.tiktok.com/@someone/video/7400000000000000000\",\n    \"person\": { \"describe\": \"a woman in her 30s, curly dark hair, denim jacket, warm smile\" },\n    \"dry_run\": true\n  }'"
      security:
      - bearerAuth: []
    servers:
    - url: https://app.hermoso.ai
components:
  schemas:
    Job:
      type: object
      properties:
        object:
          type: string
          enum:
          - job
        id:
          type: string
        type:
          type: string
          description: 'What kind of work it is: video, stitch, avatar, templatead, upscale, and so on. This set MAY GROW.'
        status:
          type: string
          description: 'queued, running, done or error. `done` and `error` are final. This enum MAY GROW: keep polling on a value you have not seen.'
        progress:
          type: number
          description: 0 to 1, when the worker reports it.
        label:
          type: string
          nullable: true
        brand_id:
          type: string
          description: The profile that started the job.
        created_at:
          type: string
          nullable: true
          description: ISO-8601.
        updated_at:
          type: string
          nullable: true
          description: ISO-8601. For a finished job, when it finished.
        run_at:
          type: string
          nullable: true
          description: ISO-8601, for work scheduled to start later; null for work that starts immediately.
        url:
          type: string
          nullable: true
          description: The finished file, once `status` is `done` and the job produced one. Always an absolute, publicly reachable URL.
        poster_url:
          type: string
          nullable: true
          description: A still for a finished video, when there is one.
        model:
          type: string
          nullable: true
          description: The model that rendered it.
        credits_used:
          type: number
          nullable: true
        error_message:
          type: string
          nullable: true
          description: Why it failed, when `status` is `error`. Named `error_message` so it can never be mistaken for this API’s error envelope.
        cancelled:
          type: boolean
          description: True when the job was stopped by `POST /v1/jobs/{id}/cancel` before it reached a model. Its `status` is `error` and nothing was charged.
        result:
          type: object
          nullable: true
          description: 'The worker’s full result. PASSTHROUGH TIER: its shape is the job type’s own and improves with it, like a tool’s `data`. Build on the fields above; read this for detail.'
        poll:
          type: string
          description: This job’s own path.
    RecastCreate:
      type: object
      required:
      - video
      - person
      properties:
        video:
          type: string
          description: 'The video the person goes into: a public https link to a video file, a TikTok, Reel, Facebook, X or YouTube post link (read once, through one lookup), or a Hermoso file URL (`POST /v1/media`, `GET /v1/library`, a job `url`).'
        person:
          $ref: '#/components/schemas/RecastPerson'
        voice:
          type: string
          description: 'Optional. A NEW voice for the new person, keeping the original words and timing: `match` (a voice that fits them), `female`, `male`, or a stock voice name (Aria, Sarah, Charlotte, George, Brian, Charlie). Omit to keep the video''s own sound. Never the original speaker''s voice or a cloned one.'
        language:
          type: string
          description: Optional. The new person speaks this language instead (for example `Spanish`), with the mouth lip-synced to the new words. `voice` defaults to `match`.
        start:
          type: number
          description: Optional. Where the recast window starts, in seconds. Default 0.
        end:
          type: number
          description: 'Optional. Where it ends, in seconds. Default: the end of the video, up to 60 seconds after `start`. At least 3 seconds.'
        instruction:
          type: string
          description: 'Optional, in your own words: who in the video is replaced when there are several people ("the man on the left").'
        dry_run:
          type: boolean
          description: true = return the exact price and the plan, and run nothing.
        callback_url:
          type: string
          description: Optional https URL to POST the finished job to (the same as the `X-Hermoso-Callback-Url` header).
    RecastQuote:
      type: object
      properties:
        object:
          type: string
          enum:
          - quote
        credits:
          type: number
          nullable: true
          description: 'The whole price: the sum of `steps`. An estimate: a real call reserves it and is charged its actual cost, usually within a few percent. null when it would not start (see `refused`).'
        likely_credits:
          type: number
          nullable: true
        refused:
          type: string
          nullable: true
          description: Why it would not start. Nothing was charged.
        steps:
          type: array
          items:
            $ref: '#/components/schemas/RecastStep'
    RecastStep:
      type: object
      properties:
        step:
          type: string
          description: '`person` (making a described AI person) or `recast`. This set MAY GROW.'
        label:
          type: string
          description: The step in plain words.
        credits:
          type: number
          nullable: true
          description: 'Its price: what the step reserves when it starts (`basis: hold`) or, once it has run, what it was charged (`basis: charged`).'
        likely_credits:
          type: number
          nullable: true
          description: 'When the hold covers an optional leg that usually does not run: the likely charge.'
        basis:
          type: string
          description: '`hold` or `charged`. This set MAY GROW.'
    RecastPerson:
      type: object
      description: Who goes into the video. Send exactly ONE of the three.
      properties:
        creator_id:
          type: string
          description: A saved creator's id or exact name, or a preset AI creator's id (the `list_creators` tool lists both). A saved photo of a real person needs a paid plan.
        image_url:
          type: string
          description: A clear photo of the person (https or a Hermoso file URL). A photo of a real person needs a paid plan, and sending it confirms you have their permission; the confirmation is recorded. A portrait Hermoso made from words for this account counts as an AI person on every plan.
        describe:
          type: string
          description: 'An AI person made from your words, on every plan: age, hair, style, vibe ("a man in his 40s, short grey hair, navy sweater"). Never a real or famous person by name. The person is made first (priced as its own step) and lands in your Library.'
    Recast:
      type: object
      properties:
        object:
          type: string
          enum:
          - recast
        dry_run:
          type: boolean
        status:
          type: string
          description: '`quoted` (a dry run), `refused` (a dry run that would not start), or the job''s own status once queued: `queued`, `running`, `done`, `error`. This enum MAY GROW.'
        quote:
          $ref: '#/components/schemas/RecastQuote'
        engine:
          type: string
          nullable: true
          description: The model that will render it.
        window:
          type: object
          nullable: true
          properties:
            start:
              type: number
            end:
              type: number
            seconds:
              type: number
          description: The part of the video that is recast, in seconds.
        source_seconds:
          type: number
          nullable: true
          description: The whole video's length.
        subject:
          type: object
          nullable: true
          properties:
            kind:
              type: string
              description: '`person`, or `object` when the picture shows no person (it then replaces the video''s subject as an object).'
            name:
              type: string
              nullable: true
        voice:
          type: object
          nullable: true
          properties:
            voice:
              type: string
            language:
              type: string
              nullable: true
            lip_synced:
              type: boolean
        notes:
          type: array
          items:
            type: string
          description: 'Plain sentences worth showing: a window that was cut, a recast done in passes and the cut second to watch, a face that is small in a shot.'
        person_url:
          type: string
          nullable: true
          description: 'With `person.describe` on a real call: the AI person that was made. It is in your Library; send it as `person.image_url` to reuse them.'
        job_id:
          type: string
          nullable: true
        job:
          allOf:
          - $ref: '#/components/schemas/Job'
          nullable: true
          description: The queued job, as `GET /v1/jobs/{id}` answers it. Poll that until `done`; `url` is the video.
    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