Mage Python SDK
Beta. This SDK is at 0.x: names and behaviour may change between minor versions until 1.0. The Mage API itself is versioned separately and is stable within
v1.
The official Python client for the Mage API. Generate images, video, and audio with Mage models from your own code. Every request is paid in Gems from your Mage account.
- Sync (
Mage) and asyncio (AsyncMage) clients on httpx run()submits a generation and waits for the result, backing off as the API recommends- Safe retries: every submit carries an idempotency key, so a retry is never charged twice
- Uploads of large files through signed URLs in one call
- Typed request and response bodies for every model, generated from the API's OpenAPI spec
Install
pip install mage-space
# or
uv add mage-space
Python 3.10 or newer.
Quick start
Create a key in API → API Keys and export it:
export MAGE_API_KEY="mage_sk_..."
from mage_space import Mage
mage = Mage()
request = mage.run("mango", {"prompt": "A lighthouse at dawn, 35mm film", "aspect_ratio": "16:9"})
print(request["result"]["url"])
Keys belong to your account and spend your Gems. Keep them on your servers: the API sends no CORS headers, and a key shipped inside an app can be extracted.
Generating
run() is generate() followed by requests.wait(). Use the parts when you want the request id before the output is ready:
request = mage.generate(
"cherry",
{
"prompt": "Waves rolling onto a black sand beach at sunset",
"resolution": "720p",
"duration": "5",
},
)
print(request["request_id"], request["status"]) # ... in_progress
final = mage.requests.wait(
request,
timeout=900, # seconds; then MageTimeoutError, and the request keeps running
on_update=lambda r: print(r["status"]),
)
if final["status"] == "completed":
print(final["result"]["url"])
wait() polls with growing intervals (2 s, then ×1.5 up to 15 s, with jitter). run() raises MageGenerationError when the request fails or is cancelled, so what it returns always has a result. Cancel a live request with mage.requests.cancel(request_id); Gems are not returned for a cancelled request.
Results are kept for 30 days. Download what you want to keep.
Async
import asyncio
from mage_space import AsyncMage
async def main() -> None:
async with AsyncMage() as mage:
request = await mage.run("seed_audio", {"prompt": "Rain on a tin roof", "duration": "10"})
print(request["result"]["url"])
asyncio.run(main())
AsyncMage has the same methods. Its on_update may be a plain function or a coroutine function.
Models
The first argument is the model's architecture id, as in its endpoint path: mango for images, cherry for video, seed_audio for audio, and many more. Choose a variant with model_id:
mage.run("mango", {"prompt": "...", "model_id": "mango-v3s"})
Each model has a TypedDict in mage_space.types with its fields, allowed values, and defaults. Annotate a config with it to have your type checker check it:
from mage_space.types import CherryConfig
config: CherryConfig = {"prompt": "A paper boat in a gutter stream", "resolution": "1080p"}
mage.run("cherry", config)
Fields a model's schema does not list pass through to the model unchanged, and a model newer than your SDK works by its id. For the live catalog with each model's options and price, call mage.architectures.list(). mage_space.ARCHITECTURES holds the name, output type, and request JSON Schema of every model this SDK version was generated from.
Inputs and uploads
Media fields (image, additional_images, first_image, last_image, video, videos) take an https URL or a data URL. Request bodies are capped at 4.5 MB, so upload larger files first and send the returned URL:
photo = mage.uploads.upload("photo.jpg") # a path, bytes, or a binary file object; 100 MB max
mage.run("kiwi", {"prompt": "The camera slowly pushes in", "first_image": photo["url"]})
clip = mage.uploads.upload("clip.mp4")
mage.run("cherry", {"prompt": "Restyle this clip as watercolor", "videos": [clip["url"]]})
The content type comes from the file name; pass content_type= for raw bytes. Uploaded files are deleted after 30 days (url_expires_at).
Characters and references
Save a character or a reference once, then mention it by @handle in any prompt:
ana = mage.characters.create(name="Ana", handle="ana", image="https://example.com/ana.png")
coat = mage.references.create(name="Red coat", handle="red-coat", kind="outfit", image=photo["url"])
mage.run("mango", {"prompt": "@ana walking through a night market, wearing @red-coat"})
page = mage.characters.list(limit=50)
while page["next_cursor"]:
page = mage.characters.list(cursor=page["next_cursor"])
mage.references.delete(coat["id"])
Errors
| Exception | When |
|---|---|
MageAPIError |
The API answered with an error: status, code (for example insufficient_gems, invalid_config), message, request_id, body. |
MageConnectionError |
No response at all (network error or timeout), after retries. |
MageGenerationError |
run() ended with a failed or cancelled request: code (for example content_blocked), reason and stage for a provider block, and the final request. |
MageTimeoutError |
wait() or run() passed its timeout. A response that finishes late is never returned; Mage checks the deadline between network reads, so a stalled read can overrun it by up to the time that was left when the read began (AsyncMage stops on time). request is the last state read (None if none was); the request keeps running. |
All four derive from MageError.
from mage_space import MageAPIError, MageGenerationError
try:
mage.run("mango", {"prompt": "..."})
except MageAPIError as error:
if error.code == "insufficient_gems":
print("Gems needed:", error.body["error"]["gems_required"])
else:
raise
except MageGenerationError as error:
print("No output:", error.code, error.message)
New error codes may appear; treat an unknown code by its HTTP status.
Provider blocks
Some models run on an external provider that can refuse a request: a real person, copyrighted material, mature content, and so on. The request fails with the code content_blocked, its Gems are refunded, and MageGenerationError.reason says why:
reason |
The provider refused | What to change |
|---|---|---|
likeness |
A real person or celebrity | Remove real people from the prompt and images |
ip |
Copyrighted or trademarked material | Describe an original design instead of a character, brand, artist, or lyrics |
nsfw |
Content past the model's own limit | Try another model, or soften the request |
minors |
Content that may involve a minor | Make sure everyone depicted is clearly an adult |
audio |
The audio, usually copyrighted music or a real person's voice | Remove music or voices from the inputs, or generate without audio |
unspecified |
Something it did not name | Reword the prompt or change the images |
stage is input when the provider refused the prompt or input media, output when it refused the generated media, and None when it did not say. Retrying the same request usually gets the same block, so change the request or the model instead. New reasons may appear; treat one you do not know as unspecified. A content_blocked request without a reason was blocked by Mage's own content policy, which keeps the Gems.
from mage_space import MageGenerationError
try:
mage.run("mango", {"prompt": "A portrait of a famous actor"})
except MageGenerationError as error:
if error.reason == "likeness":
print("Describe an original person instead, or pick another model.")
raise
Retries and idempotency
Failed calls are retried up to max_retries times (default 2) with exponential backoff:
- Reads, deletes, cancels, and uploads retry on connection errors, 408, 429, and 5xx.
generate()sends anIdempotency-Keyheader (a fresh UUID unless you passidempotency_key=) and retries with the same key on connection errors and on 408 or 5xx responses that Mage did not record. A replayed submit returns the original request and charges nothing.- Creating characters and references is never retried.
Pass your own idempotency_key, such as a job id, to make retries across processes safe too. Never reuse a key for a different request.
Configuration
Mage(
api_key=None, # default: MAGE_API_KEY
base_url=None, # default: MAGE_BASE_URL, else https://api.mage.space
timeout=60.0, # seconds per HTTP attempt
max_retries=2,
http_client=None, # your own httpx.Client (an httpx.AsyncClient for AsyncMage)
)
Use with Mage() as mage: (or mage.close()) to close the connection pool. The SDK closes only an HTTP client it created.
Documentation
- API reference: requests, inputs, errors, limits, and every model's fields
- Changelog
- Contributing
License
Metadata
Release files for mage-space 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mage_space-0.0.1.tar.gz | 55.2 kB | With provenance | Details for mage_space-0.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mage_space-0.0.1-py3-none-any.whl | Python 3 | none | any | Details for mage_space-0.0.1-py3-none-any.whl |
Total release size: 86.5 kB
Release files / mage_space-0.0.1.tar.gz
| Download URL | mage_space-0.0.1.tar.gz |
|---|---|
| Size | 55.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8de92110a3def5a363f3f46c64daab12701ebe16311372db4694f61227881838
|
|
BLAKE2b-256 checksum How to use checksums |
dd024d9591d6df8d0c248b125b2672c9eda425fe78d951af35b306328750410b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logRelease files / mage_space-0.0.1-py3-none-any.whl
| Download URL | mage_space-0.0.1-py3-none-any.whl |
|---|---|
| Size | 31.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
801ebbbe3aaeb80c7a82da62bbbcf6116b60b6879131563b4e60cec649461706
|
|
BLAKE2b-256 checksum How to use checksums |
72a8d738d7f1c1c9d11e35a4c08135897dca92b5cd516ea792ae9de299bccc83
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logSecurity for mage-space 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for mage_space-0.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.