EXPERIMENTAL — This API is in beta. We’re evaluating demand to determine scaling priorities. Your feedback is invaluable — please share your experience on Discord or via email.
Last updated: January 31, 2026
Base URL
https://amara-meshgen-api.01c.ai/meshgen-api
Authentication
All requests require a Bearer token:
Authorization: Bearer sk_amara_YOUR_API_KEY
Get your API key from the Amara web app: Profile → Admin/Developer Panel → Developer API.
Endpoints
POST {base_url} # Create generation
GET {base_url}?id={generation_id} # Check status
GET {base_url}?id={generation_id}&download=true # Download GLB
Create Generation
Start a new mesh generation from an image.
Request Body:
| Field | Type | Required | Description |
|---|
| image | string | Yes | Base64-encoded image (PNG, JPEG, or WebP) |
| mode | string | No | ”fast”, “standard”, or “detailed” (default: “fast”) |
| prompt | string | No | Text prompt to guide mesh generation |
| seed | number | No | Seed for reproducible results (default: 42) |
| decimation_target | number | No | Target triangle count, 100000-500000 (default: 300000) |
| texture_size | number | No | Texture resolution in pixels, 1024-4096 (default: 2048) |
Response:
{
"generation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "processing",
"message": "Generation started. Poll the status endpoint for updates.",
"credits_used": 1,
"credits_remaining": 99,
"key_monthly_used": 45,
"key_monthly_limit": 100
}
Use generation_id for all subsequent API calls (status polling and downloads).
key_monthly_used and key_monthly_limit only appear if your API key has a monthly limit configured.
Check Status
Poll the status of a generation.
Response (Queued):
{
"generation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"queue_position": 5,
"total_in_queue": 8
}
Response (Processing):
{
"generation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "processing",
"progress": 45,
"current_step": "Generating mesh",
"queue_position": 1,
"total_in_queue": 3
}
Response (Completed):
{
"generation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"glb_url": "https://...",
"thumbnail_url": "https://...",
"asset_name": "generated_asset_001",
"expires_in": 3600
}
expires_in is the number of seconds until glb_url expires. Download and save to your own storage immediately. queue_position indicates your place in line (1 means actively processing).
Response (Failed):
{
"generation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"error": "Generation failed",
"credits_refunded": true
}
Download GLB
Download the GLB file directly.
Response: Binary GLB file with headers:
Content-Type: model/gltf-binary
Content-Disposition: attachment; filename="asset_name.glb"
CRITICAL: You MUST download and save the GLB file to your own storage immediately after generation completes. Files are NOT retained on our servers.
Code Examples
import base64
import time
import requests
API_KEY = "sk_amara_YOUR_API_KEY"
BASE_URL = "https://amara-meshgen-api.01c.ai/meshgen-api"
def generate_mesh(image_path: str, mode: str = "fast") -> str:
"""Generate a 3D mesh from an image. Returns the generation ID."""
with open(image_path, "rb") as f:
image_base64 = base64.b64encode(f.read()).decode("utf-8")
response = requests.post(
BASE_URL,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={"image": image_base64, "mode": mode},
timeout=60,
)
response.raise_for_status()
return response.json()["generation_id"]
def poll_status(generation_id: str, timeout: int = 600) -> dict:
"""Poll until generation completes or fails."""
start = time.time()
while time.time() - start < timeout:
response = requests.get(
f"{BASE_URL}?id={generation_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
)
result = response.json()
if result["status"] in ("completed", "failed"):
return result
time.sleep(5)
raise TimeoutError("Generation timed out")
def download_glb(generation_id: str, output_path: str):
"""Download the GLB file for a completed generation."""
response = requests.get(
f"{BASE_URL}?id={generation_id}&download=true",
headers={"Authorization": f"Bearer {API_KEY}"},
stream=True,
)
response.raise_for_status()
with open(output_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
# Usage
gen_id = generate_mesh("input.png", mode="fast")
result = poll_status(gen_id)
if result["status"] == "completed":
download_glb(gen_id, "output.glb")
Best Practices
- Download & Store Immediately — You MUST download and save GLB files to your own storage immediately. Files are NOT retained on our servers.
- Poll Every 5 Seconds — Check status every 5 seconds. Handle
queue_position in the response to show users their place in line.
- Reproducible Results — Use the same
seed value to regenerate identical meshes from the same input image.
- Resize Images Client-Side — Resize to max 1024x1024 before encoding to reduce payload size and improve performance.
- Exponential Backoff — For production, implement exponential backoff with jitter when polling and retrying.
- Handle Rate Limits — Check for
retry_after_seconds in 429 responses and wait accordingly.
- Store Generation IDs — Persist generation IDs so you can recover from crashes and resume polling later.
- Tune Output Quality — Adjust
decimation_target (triangle count) and texture_size to balance quality vs file size.
Generation Modes
| Mode | Resolution | Credits | Processing Time | Best For |
|---|
| fast | 512px | 1 | ~30s-2 min | Previews, prototyping |
| standard | 1024px | 1 | ~2-4 min | Production assets |
| detailed | 1536px | 1 | ~4-8 min | High-quality final assets |
Image Requirements
| Constraint | Limit | Notes |
|---|
| Max payload size | 3MB | Base64-encoded; raw files up to ~2.25MB will fit |
| Max dimension | 1024px | Longest side; resize larger images |
| Formats | PNG, JPEG, WebP | Data URLs or raw base64 accepted |
Rate Limits
| Limit | Value | Scope |
|---|
| Generation requests | 5 per 15 minutes | Per API key |
| Monthly credits | Configurable | Per API key (optional) |
Only generation creation (POST) counts against rate limits. Status polling (GET) and downloads are NOT rate-limited, so you can poll as frequently as needed.
Error Codes
HTTP Status Codes
| Code | Meaning | Action |
|---|
| 200 | Success | Process the response |
| 400 | Bad request | Check image format/size |
| 401 | Unauthorized | Verify API key |
| 402 | Payment required | Add credits or check key limits |
| 403 | Forbidden | Key doesn’t have meshgen scope |
| 429 | Rate limited | Wait and retry |
| 503 | Service unavailable | Backend busy; credits refunded |
Error Response Codes
| Code | HTTP | Description |
|---|
| IMAGE_VALIDATION_ERROR | 400 | Image too large, invalid format, or corrupt |
| BILLING_ERROR | 402 | Organization has insufficient credits |
| KEY_LIMIT_EXCEEDED | 402 | API key’s monthly limit reached |
| CREDIT_ERROR | 402 | Failed to deduct credits |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests |
| BACKEND_TIMEOUT | 503 | Generation service timed out |
| BACKEND_ERROR | 503 | Generation service unavailable |
On 503 errors, credits are automatically refunded. Check for credits_refunded: true in the response.
Example Error Responses
400 - Image Validation Error:
{
"error": "Image too large: 5.2MB. Maximum size: 3MB.",
"code": "IMAGE_VALIDATION_ERROR",
"limits": {
"max_size_mb": 3,
"supported_formats": ["PNG", "JPEG", "WebP"]
}
}
429 - Rate Limit Exceeded:
{
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"rate_limit": {
"limit": 5,
"used": 5,
"remaining": 0,
"retry_after_seconds": 847
}
}
402 - Insufficient Credits:
{
"error": "Insufficient credits. Required: 1, Available: 0",
"code": "BILLING_ERROR",
"credits_required": 1,
"credits_remaining": 0
}
Support