> ## 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.

# Generate Image

> Generate an AI image from a prompt into your media library

## Overview

The dashboard's Media Generator, from the API: one image from a prompt, made with the same models, styles and references, at the same price, and saved to your media library. The call answers `202` right away with the new `mediaId`; the image is generated after the response. Poll [Get Media](/api-reference/media/details) until `status` is `COMPLETED` (its `url` is the image) or `FAILED`, then use the id in any create endpoint.

* `model`: an `id` of [`GET /v1/catalog/image-models`](/api-reference/catalog/get). Defaults to `gpt_image_2`.
* `style`: an `id` of `GET /v1/catalog/visual-styles` (your own `custom-<id>` styles included). Its look is added to the prompt; with no `referenceMediaIds`, a library style also sends its sample images as references, as in the dashboard.
* `referenceMediaIds`: finished images of your library for the model to keep (a person, a product, a look), up to the model's `maxReferenceImages`.

<Info>
  One image per call, as in the dashboard. It costs the model's image price (`usedCredits` in the answer); teams on their own provider keys (BYOK) are not charged and need their OpenRouter key. If the generation fails the credits come back, and one still running after 60 minutes is given up on, reads as `FAILED` and is refunded too.
</Info>

Poll Get Media every few seconds, or pass a `webhook` (with optional `metadata`) to be told once it is `COMPLETED` or `FAILED`: see [media webhooks](/guides/webhooks#media-webhooks). An image usually takes a few seconds to a minute. A `FAILED` media says why in its `error`.

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.hooked.so/v1/media/generate/image" \
    -H "x-api-key: your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{ "prompt": "A glass serum bottle on wet black stone, soft studio light, water droplets", "model": "gpt_image_2", "aspectRatio": "ratio_1_1", "name": "Serum hero shot" }'
  ```

  ```javascript JavaScript theme={null}
  const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
  const response = await fetch("https://api.hooked.so/v1/media/generate/image", {
    method: "POST",
    headers,
    body: JSON.stringify({
      prompt: "A glass serum bottle on wet black stone, soft studio light, water droplets",
      model: "gpt_image_2",
      aspectRatio: "ratio_1_1",
    }),
  });
  const { data } = await response.json();

  // Poll until the image is there
  let media;
  do {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    media = (await (await fetch(`https://api.hooked.so/v1/media/${data.mediaId}`, { headers })).json()).data;
  } while (media.status === "PROCESSING" || media.status === "PENDING");
  console.log(media.status, media.url);
  ```

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

  headers = {"x-api-key": os.environ["HOOKED_API_KEY"]}
  data = requests.post(
      "https://api.hooked.so/v1/media/generate/image",
      headers=headers,
      json={
          "prompt": "A glass serum bottle on wet black stone, soft studio light, water droplets",
          "model": "gpt_image_2",
          "aspectRatio": "ratio_1_1",
      },
  ).json()["data"]

  while True:
      time.sleep(3)
      media = requests.get(f"https://api.hooked.so/v1/media/{data['mediaId']}", headers=headers).json()["data"]
      if media["status"] in ("COMPLETED", "FAILED"):
          break
  print(media["status"], media["url"])
  ```
</RequestExample>

<ResponseExample>
  ```json 202 theme={null}
  {
    "success": true,
    "message": "Image generation started",
    "data": {
      "status": "PROCESSING",
      "mediaId": "cm4x9r2t50007ab12gh34ij78",
      "type": "image",
      "model": "gpt_image_2",
      "aspectRatio": "ratio_1_1",
      "usedCredits": 4
    }
  }
  ```

  ```json 400 theme={null}
  {
    "success": false,
    "message": "referenceMediaIds: Nano Banana takes at most 3 reference images"
  }
  ```
</ResponseExample>

## Errors

Every refusal below happens before anything is charged or created.

| Status | Message | Cause |
| - | - | - |
| 400 | `Invalid JSON body` | The body is not JSON. |
| 400 | `prompt: Required` | No prompt, or an empty one (at most 5000 characters). |
| 400 | `model: Invalid model "…"` | Not an id of `GET /v1/catalog/image-models`. |
| 400 | `style: …` | Not a visual style, or a custom style of another team. |
| 400 | `referenceMediaIds: …` | An id that is not a finished image of your library, one listed twice, or more than the model takes. |
| 401 | `code: "not_authenticated"` | Missing or invalid `x-api-key`. |
| 402 | `INSUFFICIENT_CREDITS` / `subscription_required` / `missing_credentials` | Not enough credits, no live plan, or a BYOK team without its OpenRouter key. |
| 403 | `code: "entitlement_required"` | Your team does not have the Hooked app product. |

<Tip>
  Animate the image next: pass its `mediaId` as `imageMediaId` to [Generate Video](/api-reference/media/generate-video).
</Tip>


## OpenAPI

````yaml POST /v1/media/generate/image
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/media/generate/image:
    post:
      tags:
        - Media
      summary: Generate Image
      description: >-
        The dashboard's Media Generator, one image: a prompt, a model of GET
        /v1/catalog/image-models, an optional visual style and up to the model's
        `maxReferenceImages` images of your library to keep (people, products, a
        look). Same models, price and result as the dashboard; the image lands
        in your media library. Answers 202 at once with the new `mediaId`; the
        generation runs after the response. Poll Get Media (GET
        /v1/media/{mediaId}) until `status` is `COMPLETED` (its `url` is the
        file) or `FAILED`. Or pass a `webhook` to be told once it settles
        (`media.completed` / `media.failed`) instead of polling every few
        seconds (an image takes seconds, a clip a few minutes). A generation
        that fails gives its credits back; one still running after 60 minutes is
        given up on, reads as `FAILED` and is refunded too.


        BYOK: needs your OpenRouter key.
      operationId: generateImage
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - prompt
              properties:
                prompt:
                  type: string
                  maxLength: 5000
                  description: What to generate.
                model:
                  type: string
                  default: gpt_image_2
                  description: An `id` of GET /v1/catalog/image-models.
                aspectRatio:
                  type: string
                  enum:
                    - ratio_9_16
                    - ratio_1_1
                    - ratio_16_9
                  default: ratio_9_16
                style:
                  type: string
                  description: >-
                    An `id` of GET /v1/catalog/visual-styles (your own
                    `custom-<id>` included): its look is added to the prompt.
                    Without `referenceMediaIds`, a library style also sends its
                    sample images as references.
                referenceMediaIds:
                  type: array
                  items:
                    type: string
                  description: >-
                    Ids of finished images of your media library for the model
                    to keep. At most the model's `maxReferenceImages` (400 above
                    it, or when the model takes none).
                name:
                  type: string
                  maxLength: 255
                  description: >-
                    Name in your library. Defaults to `Generated Image - <date>`
                    / `Generated Video - <date>`.
                webhook:
                  type: string
                  maxLength: 500
                  description: >-
                    Public https URL told once when the media is `COMPLETED` or
                    `FAILED` (events `media.completed` / `media.failed`, signed
                    like every Hooked webhook). See
                    [Webhooks](/guides/webhooks#media-webhooks).
                metadata:
                  type: object
                  additionalProperties: true
                  description: >-
                    Any JSON object of your own (max 5 KB). Sent back in the
                    webhook payload.
            example:
              prompt: >-
                A glass serum bottle on wet black stone, soft studio light,
                water droplets
              model: gpt_image_2
              aspectRatio: ratio_1_1
              name: Serum hero shot
      responses:
        '202':
          description: Generation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    type: object
                    properties:
                      mediaId:
                        type: string
                        description: >-
                          The new library media. Follow it with GET
                          /v1/media/{mediaId}.
                      status:
                        type: string
                        enum:
                          - PROCESSING
                        description: Always `PROCESSING` here
                      model:
                        type: string
                        description: The model generating it
                      aspectRatio:
                        type: string
                        description: The ratio being generated
                      usedCredits:
                        type: number
                        description: >-
                          Credits charged (refunded if it fails); 0 for teams on
                          their own provider keys (BYOK), who are not charged
                      type:
                        type: string
                        enum:
                          - image
              example:
                success: true
                message: Image generation started
                data:
                  status: PROCESSING
                  mediaId: cm4x9r2t50007ab12gh34ij78
                  type: image
                  model: gpt_image_2
                  aspectRatio: ratio_1_1
                  usedCredits: 4
        '400':
          description: >-
            Invalid JSON, a field that fails validation (`prompt: Required`,
            `model: Invalid model ...`, `style: ...`, `aspectRatio: ...`), more
            references than the model takes, or a `referenceMediaIds` id that is
            not a finished image of your library
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError400'
              example:
                success: false
                message: >-
                  model: Invalid model "dall_e". Allowed values are:
                  gpt_image_2, nano_banana_pro, nano_banana_2, nano_banana,
                  grok_imagine_image, seedream_5_0, seedream_4_5
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/EntitlementRequired'
        '409':
          $ref: '#/components/responses/IdempotencyInProgress'
        '422':
          $ref: '#/components/responses/IdempotencyMismatch'
        '429':
          $ref: '#/components/responses/RateLimited429'
        '500':
          description: Unexpected error; anything charged is given back
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error500'
              example:
                success: false
                message: Failed to start the image generation
      security:
        - ApiKeyAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Makes the request safe to retry. 1-255 printable ASCII characters, one
        per operation (your job id, or a UUID you store), reused on every retry.
        Within 24 hours the same key with the same body answers with the first
        response and the header `Idempotent-Replayed: true`, without running or
        charging again. Keys are scoped to your team and the endpoint. 5xx
        answers and refusals before anything ran (401, 402, 403, 409, 429) are
        not kept. See [Idempotency](/guides/idempotency).
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: ^[\x20-\x7E]+$
      example: order-1042-video
  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'
    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
    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
    MissingCredentials402:
      type: object
      description: >-
        BYOK team without the provider keys this request uses. Nothing was
        created or charged. Add the keys in Settings → AI keys.
      properties:
        success:
          type: boolean
          enum:
            - false
          description: Present when raised by the spend check
        code:
          type: string
          enum:
            - missing_credentials
        errorCode:
          type: string
          enum:
            - MISSING_CREDENTIALS
        missingProviders:
          type: array
          items:
            type: string
            enum:
              - openrouter
              - elevenlabs
              - gemini
              - bria
              - firecrawl
        message:
          type: string
        details:
          type: object
      required:
        - code
        - errorCode
        - missingProviders
        - message
      example:
        code: missing_credentials
        message: >-
          Your team uses its own provider keys (BYOK) and these are missing or
          invalid: OpenRouter, ElevenLabs. Add them in Settings → AI keys.
        errorCode: MISSING_CREDENTIALS
        missingProviders:
          - openrouter
          - elevenlabs
        details: {}
    SubscriptionRequired402:
      type: object
      description: Managed team (generates with Hooked's keys) without a live subscription.
      properties:
        success:
          type: boolean
          enum:
            - false
        code:
          type: string
          enum:
            - subscription_required
        errorCode:
          type: string
          enum:
            - SUBSCRIPTION_REQUIRED
        message:
          type: string
      required:
        - success
        - code
        - errorCode
        - message
      example:
        success: false
        code: subscription_required
        errorCode: SUBSCRIPTION_REQUIRED
        message: >-
          Your team generates with Hooked's keys, which are paid by your plan.
          Subscribe to keep generating.
    InsufficientCredits402:
      type: object
      description: >-
        Not enough credits for this request. `creditsNeeded` /
        `creditsAvailable` are included when the estimate is known.
      properties:
        success:
          type: boolean
          enum:
            - false
        errorCode:
          type: string
          enum:
            - INSUFFICIENT_CREDITS
        message:
          type: string
        creditsNeeded:
          type: number
        creditsAvailable:
          type: number
      required:
        - success
        - errorCode
        - message
      example:
        success: false
        errorCode: INSUFFICIENT_CREDITS
        message: Not enough credits to perform this action
        creditsNeeded: 40
        creditsAvailable: 12
    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
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error401'
    PaymentRequired:
      description: >-
        Payment required: a BYOK team is missing provider keys (nothing was
        created or charged), a managed team has no live subscription, or there
        are not enough credits.
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/MissingCredentials402'
              - $ref: '#/components/schemas/SubscriptionRequired402'
              - $ref: '#/components/schemas/InsufficientCredits402'
          examples:
            missingCredentials:
              value:
                code: missing_credentials
                message: >-
                  Your team uses its own provider keys (BYOK) and these are
                  missing or invalid: OpenRouter, ElevenLabs. Add them in
                  Settings → AI keys.
                errorCode: MISSING_CREDENTIALS
                missingProviders:
                  - openrouter
                  - elevenlabs
                details: {}
            subscriptionRequired:
              value:
                success: false
                code: subscription_required
                errorCode: SUBSCRIPTION_REQUIRED
                message: >-
                  Your team generates with Hooked's keys, which are paid by your
                  plan. Subscribe to keep generating.
            insufficientCredits:
              value:
                success: false
                errorCode: INSUFFICIENT_CREDITS
                message: Not enough credits to perform this action
                creditsNeeded: 40
                creditsAvailable: 12
    EntitlementRequired:
      description: The team does not have the product this endpoint needs
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EntitlementRequired403'
    IdempotencyInProgress:
      description: >-
        A request with the same `Idempotency-Key` is still running. Retry in a
        few seconds.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              code:
                type: string
              message:
                type: string
              details:
                type: object
                additionalProperties: true
            required:
              - success
              - code
              - message
          example:
            success: false
            code: idempotency_conflict
            message: A request with this Idempotency-Key is still in progress
            details: {}
    IdempotencyMismatch:
      description: >-
        The `Idempotency-Key` was already used with a different body. Use a new
        key for a new request.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              code:
                type: string
              message:
                type: string
              details:
                type: object
                additionalProperties: true
            required:
              - success
              - code
              - message
          example:
            success: false
            code: idempotency_mismatch
            message: Idempotency-Key was used with a different request
            details: {}
    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
  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.