Async Jobs
All generation requests are async. The POST endpoint returns immediately with a job_id; the actual generation runs in the background. Once done, you retrieve the result in one of three ways:
- Polling —
GET /v1/jobs/{job_id}untilstatusreaches a terminal state - Webhook — provide
callback_urlon the POST; Makistry pushes the result to your endpoint when done - Dashboard — download the file manually at api.makistry.ai
The Makistry dashboard (api.makistry.ai) gives you a visual interface over the same data the API exposes: view job status and history across all your keys, download output files directly, create and revoke API keys, rotate signing secrets, and monitor credit usage and billing. It's a useful fallback if a webhook fails or you need to retrieve a file without writing code.
Job Status
queued → processing → success
↘ failed
| Status | Meaning |
|---|---|
queued |
Job accepted and waiting for a worker |
processing |
Pipeline is actively running |
success |
Complete — download_url is available |
failed |
Irrecoverable error — error_code and error_message are set |
Typical processing times:
design_mode |
Typical duration |
|---|---|
standard |
30 s – 2 min |
pro |
1 – 5 min |
assembly |
2 – 15 min |
Set your polling timeout to at least 20 minutes for assembly jobs.
Polling — GET /v1/jobs/{job_id}
GET /v1/jobs/{job_id}
Authorization: Bearer mk_pub_...
Returns the current JobOut snapshot. The download_url is re-signed on every call — always use the URL from the most recent response.
Recommended polling interval: 5 s for the first 30 s, then 15 s until terminal state.
Python poll loop:
import time, httpx
def poll_until_done(job_id: str, timeout_s: int = 1200) -> dict:
deadline = time.time() + timeout_s
interval = 5
while time.time() < deadline:
resp = httpx.get(
f"https://api.makistry.ai/v1/jobs/{job_id}",
headers={"Authorization": "Bearer mk_pub_YOUR_KEY"},
)
resp.raise_for_status()
job = resp.json()
if job["status"] == "success":
return job
if job["status"] == "failed":
raise RuntimeError(f"{job['error_code']}: {job['error_message']}")
time.sleep(interval)
interval = min(interval * 1.5, 15) # ramp to 15s
raise TimeoutError("Job did not finish within timeout")
JobOut Fields
| Field | Populated when | Description |
|---|---|---|
job_id |
always | Unique identifier, e.g. "pub_a1b2c3d4e5f6" |
status |
always | queued | processing | success | failed |
endpoint |
always | "text_to_cad" or "image_to_cad" |
design_mode |
always | As submitted |
output_format |
always | As submitted |
created_at |
always | ISO 8601 UTC |
updated_at |
always | ISO 8601 UTC |
download_url |
success |
Signed GCS URL; re-signed on every GET. Valid for 30 days from job creation. |
expires_at |
success |
ISO 8601 expiry of the output file |
brainstorm |
success |
Design analysis object — see output-formats.md |
tokens_used |
success |
Total billable tokens consumed |
latency_ms |
success |
Wall-clock pipeline duration in milliseconds |
error_code |
failed |
Machine-readable error — see errors.md |
error_message |
failed |
Human-readable detail |
metadata |
always | Echo of the metadata you submitted |
File expiry: Output files expire 30 days after job creation. Fetching a job after expiry returns 410 output_expired. Re-submit the job to generate a fresh file.
Listing Jobs — GET /v1/jobs
GET /v1/jobs
Authorization: Bearer mk_pub_...
Query Parameters
| Parameter | Default | Description |
|---|---|---|
status |
— | Filter by status: queued, processing, success, failed |
endpoint |
— | Filter by endpoint: text_to_cad, image_to_cad |
limit |
20 |
Results per page. Range: 1–100. |
cursor |
— | Pagination cursor from next_cursor in a previous response |
Results are ordered newest first.
Pagination
When there are more results, the response includes next_cursor — an ISO 8601 timestamp. Pass it as cursor in the next request:
cursor = None
while True:
params = {"limit": 50}
if cursor:
params["cursor"] = cursor
resp = httpx.get("https://api.makistry.ai/v1/jobs",
headers=headers, params=params)
data = resp.json()
for job in data["jobs"]:
print(job["job_id"], job["status"])
cursor = data.get("next_cursor")
if not cursor:
break
Webhooks
Provide a callback_url on the POST request to receive a push notification when the job finishes (success or failure). Your endpoint must be reachable over HTTPS.
Delivery
Makistry sends a POST to your callback_url with the full JobOut payload as the body.
Your endpoint must respond with any 2xx HTTP status to acknowledge receipt. If Makistry receives a non-2xx response (or no response), it retries.
Retry policy: Up to 5 attempts with exponential backoff: 2 s → 4 s → 8 s → 16 s → 32 s.
| Retried | Not retried |
|---|---|
| 5xx responses | 4xx (except 408, 425, 429) |
| 408, 425, 429 | 3xx |
| Network errors | — |
Request Headers
X-Makistry-Signature: sha256=<hex>
X-Makistry-Timestamp: <unix seconds>
Content-Type: application/json
User-Agent: Makistry-Webhook/1.0
Verifying Signatures
Always verify webhook signatures before processing. The signature proves the request came from Makistry and was not tampered with.
Algorithm: HMAC-SHA256(signing_secret, "{timestamp}.{raw_body}")
import hashlib
import hmac
import time
def verify_makistry_webhook(
body: bytes,
sig_header: str,
ts_header: str,
signing_secret: str,
) -> bool:
# Replay protection: reject webhooks older than 5 minutes
age = abs(time.time() - int(ts_header))
if age > 300:
return False
sig = sig_header.removeprefix("sha256=")
msg = f"{ts_header}.".encode() + body
expected = hmac.new(signing_secret.encode(), msg, hashlib.sha256).hexdigest()
# Use constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected, sig)
Node.js:
import crypto from "crypto";
function verifyMakistryWebhook(body, sigHeader, tsHeader, signingSecret) {
// Replay protection
if (Math.abs(Date.now() / 1000 - parseInt(tsHeader)) > 300) return false;
const sig = sigHeader.replace("sha256=", "");
const msg = `${tsHeader}.${body}`;
const expected = crypto.createHmac("sha256", signingSecret).update(msg).digest("hex");
// timingSafeEqual throws if buffers differ in length — catch and return false
try {
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
} catch {
return false; // length mismatch = invalid signature
}
}
Example Webhook Payload (success)
{
"job_id": "pub_a1b2c3d4e5f6",
"status": "success",
"endpoint": "text_to_cad",
"design_mode": "standard",
"output_format": "STEP",
"created_at": "2026-05-30T10:00:00.000000+00:00",
"updated_at": "2026-05-30T10:01:15.000000+00:00",
"download_url": "https://storage.googleapis.com/...",
"expires_at": "2026-06-29T10:00:00.000000+00:00",
"brainstorm": {
"project_name": "Lens Cap — 60mm OD",
"key_features": ["snap-fit retention ring", "52mm inner lip"],
"design_components": ["cap body", "retention ring"],
"optimal_geometry": "Thin-wall cylindrical extrusion with inset radial lip",
"material_recommendations": "PLA or ABS for FDM",
"suggestions": ["Add a tether boss", "Chamfer the outer rim"]
},
"tokens_used": 8420,
"latency_ms": 74300,
"metadata": {}
}
Example Webhook Payload (failure)
{
"job_id": "pub_a1b2c3d4e5f6",
"status": "failed",
"endpoint": "text_to_cad",
"design_mode": "standard",
"error_code": "geometry_validation_failed",
"error_message": "Contract gate rejected: contradictory dimension constraints detected",
"metadata": {}
}