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 requests
url = "https://api.mage.space/v1/characters"
payload = {
"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"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)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": "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 requests
url = "https://api.mage.space/v1/characters"
payload = {
"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"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)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": "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. 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
An API key from Settings → API, sent 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. 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.

