curl --request POST \
--url https://api.mage.space/v1/characters \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"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"
}
'import Mage from '@mage-space/sdk';
const mage = new Mage(); // reads MAGE_API_KEY
await mage.characters.create({
name: 'Ana',
handle: 'ana',
image: 'https://example.com/ana.jpg',
});
await mage.run('mango', { prompt: '@ana walking through a night market' });from mage_space import Mage
mage = Mage() # reads MAGE_API_KEY
mage.characters.create(
name="Ana",
handle="ana",
image="https://example.com/ana.jpg",
)
mage.run("mango", {"prompt": "@ana walking through a night market"})const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
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'
})
};
fetch('https://api.mage.space/v1/characters', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"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"
}{
"error": {
"code": "invalid_request",
"message": "`name` is required."
}
}{
"error": {
"code": "unauthorized",
"message": "The API key is invalid or has been revoked."
}
}{
"error": {
"code": "payment_required",
"message": "The Mage API and MCP server need a successful payment on this account first: buy any Gem pack or start a plan at https://www.mage.space/gems, then retry. Signup bonus Gems work in the Mage app only."
}
}{
"error": {
"code": "forbidden",
"message": "This account cannot create characters or references."
}
}{
"error": {
"code": "handle_taken",
"message": "The handle `@ana` is taken. Choose another, or omit `handle` to derive one from the name.",
"handle": "ana"
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong on our side. Retry, and contact support if it persists."
}
}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.
curl --request POST \
--url https://api.mage.space/v1/characters \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"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"
}
'import Mage from '@mage-space/sdk';
const mage = new Mage(); // reads MAGE_API_KEY
await mage.characters.create({
name: 'Ana',
handle: 'ana',
image: 'https://example.com/ana.jpg',
});
await mage.run('mango', { prompt: '@ana walking through a night market' });from mage_space import Mage
mage = Mage() # reads MAGE_API_KEY
mage.characters.create(
name="Ana",
handle="ana",
image="https://example.com/ana.jpg",
)
mage.run("mango", {"prompt": "@ana walking through a night market"})const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
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'
})
};
fetch('https://api.mage.space/v1/characters', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"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"
}{
"error": {
"code": "invalid_request",
"message": "`name` is required."
}
}{
"error": {
"code": "unauthorized",
"message": "The API key is invalid or has been revoked."
}
}{
"error": {
"code": "payment_required",
"message": "The Mage API and MCP server need a successful payment on this account first: buy any Gem pack or start a plan at https://www.mage.space/gems, then retry. Signup bonus Gems work in the Mage app only."
}
}{
"error": {
"code": "forbidden",
"message": "This account cannot create characters or references."
}
}{
"error": {
"code": "handle_taken",
"message": "The handle `@ana` is taken. Choose another, or omit `handle` to derive one from the name.",
"handle": "ana"
}
}{
"error": {
"code": "internal_error",
"message": "Something went wrong on our side. Retry, and contact support if it persists."
}
}name, an image (an https URL, an 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. Handles such as image1 and image2 are reserved for uploaded images and return 400 invalid_config. 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.Authorizations
Create a key in API → API Keys: select Create API key, name it in the popup, then select Create key. Copy the one-time key and send it as Authorization: Bearer mage_sk_…. Keys are for server-side code only.
Body
The display name.
1 - 50The portrait: an https URL, an upload URL, or a data URL of a JPEG or PNG. Stored permanently with the character.
The @handle prompts mention it by: 1 to 15 lowercase letters, digits, underscores, or dashes, starting with a letter. Handles such as image1 and image2 are reserved for uploaded images. Derived from the name when omitted. Cannot be changed later.
Optional notes, for your own reference.
500Optional 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.
Response
The character.
The character id, as DELETE /v1/characters/{character_id} takes it.
Mention the character in a prompt as @handle.
The display name.
Your notes, or null.
The portrait.
The processed voice clip, or null for a character without one.
Characters created through the API are private; publishing happens in the app.
public, private When it was created.

