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

# Create character

> POST /v1/characters saves a private character from a name, a portrait image, and optionally a voice clip, and returns the handle a prompt mentions it by.

Save a character: a `name`, an `image` (an https URL, an [upload](/api/endpoints/create-upload) URL, or a data URL of a JPEG or PNG), and optionally a `description` and a `voice`. The image is stored with the character for as long as it exists, unlike request inputs, which expire after 30 days.

The `handle` is what a prompt mentions as `@handle`: 1 to 15 lowercase letters, digits, underscores, or dashes, starting with a letter, and unique among live characters and references. Leave it out to have one derived from the name with a short random suffix. A handle that is already in use is a `409 handle_taken`; retry with another. Handles cannot be changed later.

A `voice` clip (MP3 or WAV) goes through the same processing as a clip uploaded in the app: it is trimmed to 10 seconds and normalized, and the processed clip is what the models hear. On models that support character voices, the voice is sent whenever the character is mentioned unless the request sets `use_character_voices` to false.

Characters created through the API are private. Publishing them happens in the app, which runs media moderation.


## OpenAPI

````yaml api/openapi.json POST /v1/characters
openapi: 3.1.0
info:
  title: Mage API
  version: v1
  description: >-
    Run Mage models from your own code: submit a generation, poll its status,
    download the result. Every request is paid in gems.
  contact:
    name: Mage
    url: https://www.mage.space
    email: mage@mage.space
servers:
  - url: https://api.mage.space
    description: Production
security:
  - apiKey: []
tags:
  - name: Requests
    description: Submit, poll, and cancel generations.
  - name: Catalog
    description: What the API can generate with.
  - name: Account
    description: The gems balance.
  - name: Uploads
    description: Large input files.
  - name: Characters
    description: Saved characters a prompt mentions by `@handle`.
  - name: References
    description: Saved image and audio references a prompt mentions by `@handle`.
  - name: Image models
    description: One endpoint per image architecture.
  - name: Video models
    description: One endpoint per video architecture.
paths:
  /v1/characters:
    post:
      tags:
        - Characters
      summary: Create character
      description: >-
        Save a private character from a name, a portrait, and optionally a voice
        clip. Its `handle` is what a prompt mentions.
      operationId: create_character
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCharacterRequest'
            example:
              name: Ana
              handle: ana
              image: https://example.com/ana.jpg
              description: A red-haired courier in a green jacket.
              voice: https://example.com/ana-voice.mp3
      responses:
        '201':
          description: The character.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Character'
              example:
                id: 3f9c2a1e-7b44-4d2e-9a0f-6c1d8e2b5a71
                handle: ana
                name: Ana
                description: A red-haired courier in a green jacket.
                image_url: https://cdn3.mage.space/characters/user/image/9b1e.jpg
                voice_url: https://cdn3.mage.space/references/user/audio/7d2c.mp3
                visibility: private
                created_at: '2026-09-18T10:00:00.000Z'
        '400':
          $ref: '#/components/responses/InvalidCreateRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/CreateForbidden'
        '409':
          $ref: '#/components/responses/HandleTaken'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  schemas:
    CreateCharacterRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 50
          description: The display name.
        handle:
          description: >-
            The `@handle` prompts mention it by: 1 to 15 lowercase letters,
            digits, underscores, or dashes, starting with a letter. Derived from
            the name when omitted. Cannot be changed later.
          type: string
        image:
          anyOf:
            - type: string
              format: uri
            - type: string
              pattern: ^data:[^;,]+(?:;[^;,]+)*;base64,
          description: >-
            The portrait: an https URL, an upload URL, or a data URL of a JPEG
            or PNG. Stored permanently with the character.
        description:
          description: Optional notes, for your own reference.
          type: string
          maxLength: 500
        voice:
          description: >-
            Optional voice clip: an https URL, an upload URL, or a data URL of
            an MP3 or WAV. Trimmed to 10 seconds and normalized, like a clip
            uploaded in the app.
          anyOf:
            - type: string
              format: uri
            - type: string
              pattern: ^data:[^;,]+(?:;[^;,]+)*;base64,
      required:
        - name
        - image
      additionalProperties: false
    Character:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: >-
            The character id, as `DELETE /v1/characters/{character_id}` takes
            it.
        handle:
          type: string
          description: Mention the character in a prompt as `@handle`.
        name:
          type: string
          description: The display name.
        description:
          anyOf:
            - type: string
            - type: 'null'
          description: Your notes, or null.
        image_url:
          type: string
          format: uri
          description: The portrait.
        voice_url:
          anyOf:
            - type: string
              format: uri
            - type: 'null'
          description: The processed voice clip, or null for a character without one.
        visibility:
          type: string
          enum:
            - public
            - private
          description: >-
            Characters created through the API are private; publishing happens
            in the app.
        created_at:
          type: string
          format: date-time
          description: When it was created.
      required:
        - id
        - handle
        - name
        - description
        - image_url
        - voice_url
        - visibility
        - created_at
      additionalProperties: false
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - invalid_request
                - invalid_config
                - architecture_not_found
                - architecture_retired
                - request_not_found
                - character_not_found
                - reference_not_found
                - not_found
                - insufficient_gems
                - forbidden
                - content_blocked
                - request_finished
                - handle_taken
                - too_many_requests
                - generation_failed
                - internal_error
              description: A stable error code; treat an unknown code by its HTTP status.
            message:
              type: string
              description: What went wrong, for a person.
            request_id:
              description: >-
                The request the refusal belongs to, when one was recorded before
                the refusal.
              type: string
              format: uuid
            gems_required:
              description: The price of the refused generation, on `insufficient_gems`.
              type: number
            docs_url:
              description: Where to read the API reference, on `not_found`.
              type: string
              format: uri
            handle:
              description: The handle that is taken, on `handle_taken`.
              type: string
          required:
            - code
            - message
          additionalProperties: true
          description: Further fields depend on the code.
      required:
        - error
      additionalProperties: false
  responses:
    InvalidCreateRequest:
      description: >-
        The body is not a JSON object, a required field is missing, a value is
        out of range, `handle` breaks the handle rules, or a media source could
        not be fetched or is not an accepted format. The message names the
        field.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: invalid_request
              message: '`name` is required.'
    Unauthorized:
      description: The key is missing, malformed, unknown, or revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unauthorized
              message: The API key is invalid or has been revoked.
    CreateForbidden:
      description: The account cannot create characters or references.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: forbidden
              message: This account cannot create characters or references.
    HandleTaken:
      description: >-
        A live character or reference already uses the handle. Choose another,
        or omit `handle` to derive one from the name.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: handle_taken
              message: >-
                The handle `@ana` is taken. Choose another, or omit `handle` to
                derive one from the name.
              handle: ana
    InternalError:
      description: >-
        Something failed on Mage's side. Retry, and contact support if it
        persists.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: internal_error
              message: >-
                Something went wrong on our side. Retry, and contact support if
                it persists.
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        An API key from Settings → API, sent as `Authorization: Bearer
        mage_sk_…`. Keys are for server-side code only.

````