> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hooked.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Edit Project

> Change a finished project's captions, music or logo, then render it again

## Overview

Makes a few targeted changes to a finished project. Each one is saved to the project's editor timeline the same way the Hooked editor saves it, so the project opens in the dashboard with your changes.

What you can edit through the API:

| Field | What it changes |
| - | - |
| `caption.preset` | The caption style, by an id of [`caption-presets`](/api-reference/catalog/get). It brings the style's look, box size and entrance animation, and re-cuts the lines to fit |
| `caption.alignment` | Where the captions sit: `top`, `middle` or `bottom` |
| `caption.disabled` | `true` hides the captions in the render, `false` shows them again. Nothing is deleted |
| `captionSegments` | New text for caption lines, by the `index` [Get Project Captions](/api-reference/project/captions) answered |
| `music.musicId` | Another background track from [List Music](/api-reference/music/list), or `null` to remove the music |
| `music.volume` | Music volume, `0` to `1` |
| `branding.logoMediaId` | An image of your [media library](/api-reference/media/list) as the logo, or `null` to remove it |
| `branding.position`, `branding.size` | Move the logo to a corner, or resize it (20 to 200 px) |

**Everything else is edited in the dashboard**: clips and images, the voice and the script, timing, text layers, transitions, colours and fonts of a caption style. A field this endpoint does not know is refused with a `400` that names it, and nothing is saved.

Editing is free and does not render anything. When you are done, call [Render Project](/api-reference/video/render) to get a new video.

## How each edit is applied

* **Caption text.** The line keeps its start and end time; its words share that time evenly, as when you retype a line in the editor. Read the lines first with [Get Project Captions](/api-reference/project/captions): `index` counts from `0` across the whole video.
* **Caption style.** A project made from a template has one caption layer per scene; the style, alignment and switch apply to all of them. A new style re-cuts the lines to its box, so line numbers change: read the captions again before correcting text. In one request, `captionSegments` is applied first, with the indexes you read.
* **Music.** The new track takes the old one's place and timing, and plays from its start. A project without music gets the track from start to end at volume `0.5`. A track shorter than the video stops when it ends. The song of a [Music to Video](/api-reference/video/music-to-video) project cannot be swapped (its volume can).
* **Logo.** The new image replaces the logo where it is. Without a logo yet, `logoMediaId` adds one over the whole video, `bottom-right` at 100 px unless you say otherwise. The image must be `COMPLETED` in your library.

<Warning>
  If the project is open in the dashboard editor, its autosave can overwrite what you change here. Close it, or reload it after editing.
</Warning>

## When it is refused

| Status | `message` | What to do |
| - | - | - |
| `409` | `Project is still processing` | Wait until the project is `completed` or `failed` ([Get Project](/api-reference/project/details)). |
| `409` | `A video of this project is still rendering` | Wait until its `video.status` is `COMPLETED` or `FAILED`. |
| `409` | `The project changed while it was being edited…` | Someone saved the project (in the dashboard) during your request. Read it again and retry. |
| `400` | `There is nothing on the timeline to edit yet` | The project has not finished generating. |
| `400` | `caption: The project has no captions` | Captions cannot be added through the API. |

<RequestExample>
  ```bash cURL theme={null}
  curl -X PATCH "https://api.hooked.so/v1/project/cm4x9k2p10001ab12cd34ef56/edit" \
    -H "x-api-key: your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "captionSegments": [{ "index": 1, "text": "changed my mornings forever" }],
      "caption": { "preset": "beast", "alignment": "bottom" },
      "music": { "musicId": "12", "volume": 0.3 },
      "branding": { "logoMediaId": "cm4x9p3r40005ab12gh56ij78", "position": "top-right" }
    }'
  ```

  ```javascript JavaScript theme={null}
  const projectId = "cm4x9k2p10001ab12cd34ef56";
  const headers = {
    "x-api-key": process.env.HOOKED_API_KEY,
    "Content-Type": "application/json",
  };

  // 1. Fix a caption line and lower the music
  const edit = await fetch(`https://api.hooked.so/v1/project/${projectId}/edit`, {
    method: "PATCH",
    headers,
    body: JSON.stringify({
      captionSegments: [{ index: 1, text: "changed my mornings forever" }],
      music: { volume: 0.3 },
    }),
  });
  const edited = await edit.json();
  if (!edited.success) throw new Error(edited.message);

  // 2. Render the edited project
  const render = await fetch("https://api.hooked.so/v1/render", {
    method: "POST",
    headers,
    body: JSON.stringify({ projectId }),
  });
  const { data } = await render.json();
  console.log("New video:", data.videoId);
  ```

  ```python Python theme={null}
  import os
  import requests

  project_id = "cm4x9k2p10001ab12cd34ef56"
  headers = {"x-api-key": os.environ["HOOKED_API_KEY"]}

  edited = requests.patch(
      f"https://api.hooked.so/v1/project/{project_id}/edit",
      headers=headers,
      json={
          "caption": {"preset": "beast"},
          "branding": {"logoMediaId": "cm4x9p3r40005ab12gh56ij78", "position": "top-right"},
      },
  ).json()
  if not edited["success"]:
      raise RuntimeError(edited["message"])

  render = requests.post(
      "https://api.hooked.so/v1/render",
      headers=headers,
      json={"projectId": project_id},
  ).json()
  print("New video:", render["data"]["videoId"])
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "success": true,
    "message": "Project edited",
    "data": {
      "projectId": "cm4x9k2p10001ab12cd34ef56",
      "changed": ["captionSegments", "caption.preset", "caption.alignment", "music.musicId", "music.volume", "branding.logoMediaId", "branding.position"],
      "caption": { "disabled": false, "preset": "beast", "alignment": "bottom", "segments": 14 },
      "music": { "musicId": "12", "name": "Arietta", "volume": 0.3 },
      "branding": { "logoMediaId": "cm4x9p3r40005ab12gh56ij78", "position": "top-right", "size": 100 }
    }
  }
  ```

  ```json 400 theme={null}
  {
    "success": false,
    "message": "voiceId: Cannot be edited through the API. Only caption, captionSegments, music and branding can; edit anything else in the dashboard"
  }
  ```

  ```json 409 theme={null}
  {
    "success": false,
    "message": "A video of this project is still rendering"
  }
  ```

  ```json 404 theme={null}
  {
    "success": false,
    "message": "Project not found"
  }
  ```
</ResponseExample>

## Response

`data` shows the editable parts of the project after the edit: `caption`, `music` and `branding` are `null` when the project has none. `changed` lists what this request changed. `branding.logoMediaId` is `null` for a logo added when the project was created.

## Errors

| Status | When |
| - | - |
| 400 | Invalid edit, as `<field>: <reason>`: an unknown field, an unknown caption style, a segment index out of range, a music or logo id that is not yours, an image still importing. Nothing was saved |
| 401 | Missing or invalid API key (`code: "not_authenticated"`) |
| 403 | The team does not have the Hooked app product (`code: "entitlement_required"`) |
| 404 | No such project. Another team's project, or an id that is not one, answers the same `Project not found` |
| 409 | The project is generating, one of its videos is rendering, or it was saved elsewhere during the request. Nothing was saved |
| 500 | `Internal server error`. Read the captions or the project before retrying |


## OpenAPI

````yaml PATCH /v1/project/{projectId}/edit
openapi: 3.0.0
info:
  title: Hooked API
  version: 1.0.0
  description: AI Video Generation API
servers:
  - url: https://api.hooked.so
security:
  - ApiKeyAuth: []
tags:
  - name: Videos
    description: Create projects, follow them and render videos
  - name: Account Analysis
    description: Analyze TikTok and YouTube accounts
  - name: Channels
    description: Connected social accounts you publish to
  - name: Publish
    description: Schedule videos to your connected accounts
  - name: Images
    description: 'Image editing: background removal and expansion'
  - name: Media
    description: 'Your media library: list, import from a URL, upload a file'
  - name: Avatars
    description: Your team's own avatars (Actors) and their Hook + Demo reactions
paths:
  /v1/project/{projectId}/edit:
    patch:
      tags:
        - Videos
      summary: Edit Project
      description: >-
        A few targeted changes to a finished project, saved to its editor
        timeline exactly as the dashboard editor saves them: caption style,
        position and on/off, the text of caption lines, the background music
        (track and volume) and the branding logo. Free, and nothing is rendered:
        call POST /v1/render afterwards for a new video. Anything else (clips,
        voice, script, timing, text layers...) is edited in the dashboard.
        Refused with 409 while the project is generating, while one of its
        videos renders, or when the timeline was saved elsewhere (the dashboard
        editor) during the request.
      operationId: editProject
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
          description: >-
            The `projectId` a create endpoint answered, or one from GET
            /v1/project/list.
          example: cm4x9k2p10001ab12cd34ef56
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              description: >-
                Send at least one of the four groups. A field that is not listed
                here is refused with a 400: edit anything else in the dashboard.
              properties:
                caption:
                  type: object
                  additionalProperties: false
                  description: >-
                    The captions' style, position and on/off switch. Applies to
                    every caption layer of the project (a template project has
                    one per scene).
                  properties:
                    preset:
                      type: string
                      description: >-
                        A caption style: an id of GET
                        /v1/catalog/caption-presets (`classic`, `beast`...).
                        Sets the style's look, box and entrance animation, and
                        re-cuts the lines to fit, so segment numbers change:
                        read [Get Project
                        Captions](/api-reference/project/captions) again before
                        correcting text. Any colour or font tweaks made in the
                        editor are replaced by the style's.
                      example: beast
                    alignment:
                      type: string
                      enum:
                        - top
                        - middle
                        - bottom
                      description: >-
                        Move the captions to the top, middle or bottom of the
                        frame.
                      example: bottom
                    disabled:
                      type: boolean
                      description: >-
                        `true` hides the captions in the render; `false` shows
                        them again. Nothing is deleted.
                      example: false
                captionSegments:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: >-
                    New text for caption lines, by the `index` [Get Project
                    Captions](/api-reference/project/captions) answered (each
                    index once). The line keeps its start and end; its words
                    share that time evenly, as when you retype a line in the
                    editor. Applied before `caption.preset`, so the indexes are
                    the ones you read.
                  items:
                    type: object
                    additionalProperties: false
                    required:
                      - index
                      - text
                    properties:
                      index:
                        type: integer
                        minimum: 0
                        description: The segment's `index`.
                        example: 1
                      text:
                        type: string
                        maxLength: 500
                        description: The corrected line. Not empty.
                        example: changed my mornings forever
                music:
                  type: object
                  additionalProperties: false
                  description: >-
                    The background music. Not available for the song of a Music
                    to Video project.
                  properties:
                    musicId:
                      type: string
                      nullable: true
                      description: >-
                        A track of GET /v1/music/list (library or your own
                        uploads) in place of the current one, with the same
                        timing; `null` removes the music. When the project has
                        no music, the track is added from start to end at volume
                        0.5. A track shorter than the video stops when it ends.
                      example: '12'
                    volume:
                      type: number
                      minimum: 0
                      maximum: 1
                      description: >-
                        Music volume, 0 (muted) to 1 (full). Not with `musicId:
                        null`.
                      example: 0.3
                branding:
                  type: object
                  additionalProperties: false
                  description: >-
                    The logo over the video (the branding logo a create request
                    can add). Without a logo yet, `logoMediaId` adds one over
                    the whole video.
                  properties:
                    logoMediaId:
                      type: string
                      nullable: true
                      description: >-
                        An image of your media library (GET /v1/media/list,
                        `COMPLETED`) as the logo; `null` removes the logo.
                      example: cm4x9p3r40005ab12gh56ij78
                    position:
                      type: string
                      enum:
                        - top-left
                        - top-right
                        - bottom-left
                        - bottom-right
                      description: >-
                        The corner the logo sits in. New logos default to
                        `bottom-right`.
                      example: top-right
                    size:
                      type: integer
                      minimum: 20
                      maximum: 200
                      description: >-
                        The logo's width and height in pixels. New logos default
                        to 100.
                      example: 120
            example:
              caption:
                preset: beast
                alignment: bottom
              captionSegments:
                - index: 1
                  text: changed my mornings forever
              music:
                musicId: '12'
                volume: 0.3
              branding:
                logoMediaId: cm4x9p3r40005ab12gh56ij78
                position: top-right
      responses:
        '200':
          description: The project was edited
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    type: object
                    required:
                      - projectId
                      - changed
                      - caption
                      - music
                      - branding
                    properties:
                      projectId:
                        type: string
                      changed:
                        type: array
                        items:
                          type: string
                        description: >-
                          What this request changed, by field (`caption.preset`,
                          `captionSegments`, `music.volume`...).
                      caption:
                        type: object
                        nullable: true
                        description: The captions now, or `null` when the project has none.
                        properties:
                          disabled:
                            type: boolean
                            description: >-
                              `true` when the captions are switched off (they
                              stay in the project and render again once switched
                              on).
                          preset:
                            type: string
                            nullable: true
                            description: >-
                              The caption style (an id of GET
                              /v1/catalog/caption-presets), or `null` when
                              unknown.
                          alignment:
                            type: string
                            nullable: true
                            enum:
                              - top
                              - middle
                              - bottom
                            description: >-
                              Where the captions sit: `top`, `middle` or
                              `bottom`.
                          segments:
                            type: integer
                            description: How many caption lines there are now.
                      music:
                        type: object
                        nullable: true
                        description: >-
                          The background music now, or `null` when there is
                          none.
                        properties:
                          musicId:
                            type: string
                            nullable: true
                            description: >-
                              The track's id (as in GET /v1/music/list), when
                              known.
                          name:
                            type: string
                            nullable: true
                          volume:
                            type: number
                      branding:
                        type: object
                        nullable: true
                        description: The logo now, or `null` when there is none.
                        properties:
                          logoMediaId:
                            type: string
                            nullable: true
                            description: >-
                              The library image, when it was set through this
                              endpoint; `null` for a logo added at creation.
                          position:
                            type: string
                            enum:
                              - top-left
                              - top-right
                              - bottom-left
                              - bottom-right
                          size:
                            type: integer
              example:
                success: true
                message: Project edited
                data:
                  projectId: cm4x9k2p10001ab12cd34ef56
                  changed:
                    - captionSegments
                    - caption.preset
                    - caption.alignment
                    - music.musicId
                    - music.volume
                    - branding.logoMediaId
                    - branding.position
                  caption:
                    disabled: false
                    preset: beast
                    alignment: bottom
                    segments: 14
                  music:
                    musicId: '12'
                    name: Arietta
                    volume: 0.3
                  branding:
                    logoMediaId: cm4x9p3r40005ab12gh56ij78
                    position: top-right
                    size: 100
        '400':
          description: 'Invalid edit; nothing was saved. `message` is `<field>: <reason>`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError400'
              examples:
                unknownField:
                  value:
                    success: false
                    message: >-
                      voiceId: Cannot be edited through the API. Only caption,
                      captionSegments, music and branding can; edit anything
                      else in the dashboard
                preset:
                  value:
                    success: false
                    message: >-
                      caption.preset: Invalid caption preset "fancy". Allowed
                      values are: default, classic, ...
                segment:
                  value:
                    success: false
                    message: >-
                      captionSegments.0.index: No segment 40; the project has 14
                      (0 to 13)
                noCaptions:
                  value:
                    success: false
                    message: 'caption: The project has no captions'
                music:
                  value:
                    success: false
                    message: 'music.musicId: Music "999" not found'
                logo:
                  value:
                    success: false
                    message: >-
                      branding.logoMediaId: Media "cm4x9p3r40005ab12gh56ij78"
                      not found
                empty:
                  value:
                    success: false
                    message: There is nothing on the timeline to edit yet
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EntitlementRequired'
        '404':
          description: >-
            Not found. Another team's project, or an id that is not one, answers
            the same way as a missing one.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotFound404'
              example:
                success: false
                message: Project not found
        '409':
          description: The project cannot be edited right now; nothing was saved
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - message
                properties:
                  success:
                    type: boolean
                    enum:
                      - false
                  message:
                    type: string
              examples:
                processing:
                  value:
                    success: false
                    message: Project is still processing
                rendering:
                  value:
                    success: false
                    message: A video of this project is still rendering
                changed:
                  value:
                    success: false
                    message: >-
                      The project changed while it was being edited. Read it
                      again and retry
        '429':
          $ref: '#/components/responses/RateLimited429'
        '500':
          $ref: '#/components/responses/InternalError'
      security:
        - ApiKeyAuth: []
components:
  schemas:
    ValidationError400:
      type: object
      description: >-
        The request is invalid. `message` is `<field>: <reason>` when a field is
        to blame.
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
      required:
        - success
        - message
      example:
        success: false
        message: 'webhook: Must be a valid HTTPS URL'
    NotFound404:
      type: object
      description: >-
        Not found. Another team's resource answers the same way as a missing
        one.
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
      required:
        - success
        - message
      example:
        success: false
        message: Project not found
    Error401:
      type: object
      description: Missing or invalid `x-api-key`.
      properties:
        code:
          type: string
          enum:
            - not_authenticated
        message:
          type: string
          example: Not authenticated
        errorCode:
          type: string
          enum:
            - NOT_AUTHENTICATED
        details:
          type: object
          additionalProperties:
            type: string
      example:
        code: not_authenticated
        message: Not authenticated
        errorCode: NOT_AUTHENTICATED
        details:
          x-api-key: Header not provided or API Key invalid
    EntitlementRequired403:
      type: object
      description: >-
        Valid key, but the team does not have the product this endpoint belongs
        to.
      properties:
        code:
          type: string
          enum:
            - entitlement_required
        error:
          type: string
          enum:
            - entitlement_required
        message:
          type: string
        product:
          type: string
          example: app
      required:
        - code
        - error
        - message
        - product
      example:
        code: entitlement_required
        error: entitlement_required
        message: This endpoint requires the "app" product.
        product: app
    Error500:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
        error:
          type: string
          description: Present on some endpoints
      example:
        success: false
        message: Internal server error
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error401'
    EntitlementRequired:
      description: The team does not have the product this endpoint needs
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EntitlementRequired403'
    RateLimited429:
      description: >-
        Your team is over a rate limit: 30 POST requests per minute, 120 other
        requests per minute, or 3 downloads of your URLs running at once.
        Nothing ran and nothing was charged: wait `Retry-After` seconds and send
        the request again. See [Rate
        limits](/guides/error-handling#rate-limits-429).
      headers:
        Retry-After:
          description: Seconds to wait before sending again.
          schema:
            type: integer
        X-RateLimit-Limit:
          description: The limit of the bucket you hit (not sent for the download cap).
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: 'Requests left in this window: 0 (not sent for the download cap).'
          schema:
            type: integer
        X-RateLimit-Reset:
          description: >-
            When the window ends, in Unix seconds (not sent for the download
            cap).
          schema:
            type: integer
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              errorCode:
                type: string
                enum:
                  - RATE_LIMITED
              message:
                type: string
              retryAfterSeconds:
                type: integer
            required:
              - success
              - errorCode
              - message
              - retryAfterSeconds
          example:
            success: false
            errorCode: RATE_LIMITED
            message: >-
              Rate limit exceeded: 30 POST requests per minute per team. Retry
              after 12 s.
            retryAfterSeconds: 12
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error500'
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.