Mockly API
POST /api/v1/chat/render
Submit a chat mockup for rendering. Image renders return synchronously; video renders return immediately and are polled.
Endpoint
POST https://www.getmockly.com/api/v1/chat/render
Authorization: Bearer mk_live_...
Content-Type: application/json
Body: a ChatExportPayload — see Schema for the full reference.
Body size limit: 10 MB. If you have many image messages encoded as data URLs you may hit this — host the images and pass URLs instead.
Response shape depends on renderKind:
"image"→200 OKsynchronously withdownloadUrlandsizeBytes."video"→202 Acceptedimmediately withstatus: "rendering"and astatusUrlto poll.
Image render
Image renders return as soon as the still is generated (typically sub-second on Lambda). The response includes a downloadUrl you can fetch directly:
curl -X POST https://www.getmockly.com/api/v1/chat/render \
-H "Authorization: Bearer mk_live_..." \
-H "Content-Type: application/json" \
-d @payload.json
// 200 OK
{
"id": "abc-123-...",
"status": "completed",
"renderKind": "image",
"format": "png",
"downloadUrl": "https://...",
"sizeBytes": 482113
}
format is always "png" for images. downloadUrl is a short-lived signed URL — see URL expiry.
Video render
Video renders are async because they take 30 seconds to a few minutes to encode. The POST returns immediately with a statusUrl:
// 202 Accepted
{
"id": "abc-123-...",
"status": "rendering",
"renderKind": "video",
"statusUrl": "https://www.getmockly.com/api/v1/exports/abc-123-..."
}
Set renderKind: "video" in the request. For transparent-background video, set props.transparentBackground: true — the output will be WebM (with alpha) instead of MP4.
Polling pattern
Poll the statusUrl every 2–4 seconds until you see status: "completed". Don't keep the original POST connection open — it would hold a render slot for nothing.
const submit = await fetch("https://www.getmockly.com/api/v1/chat/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOCKLY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const { id, statusUrl } = await submit.json();
// Poll until done
while (true) {
await new Promise((r) => setTimeout(r, 3000));
const status = await fetch(statusUrl, {
headers: { Authorization: `Bearer ${process.env.MOCKLY_API_KEY}` },
});
const data = await status.json();
if (data.status === "completed") {
console.log("Done:", data.downloadUrl);
break;
}
if (data.status === "failed") {
throw new Error(data.error);
}
console.log(`Rendering — ${Math.round((data.progress ?? 0) * 100)}%`);
}
Recommended: cap your polling to 10 minutes total (videos rarely take longer). After that, treat as failed and retry the render.
Rate limits
Per-minute rate limits are enforced per API key:
| Endpoint | Limit |
|---|---|
POST /api/v1/chat/render | 60 requests / min |
GET /api/v1/exports/:id and /exports/:id/download | 120 requests / min |
When exceeded, you'll get 429 rate_limited with these headers:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait before retrying. |
X-RateLimit-Limit | The limit (e.g. 60). |
X-RateLimit-Remaining | Requests left in the current window (usually 0 on a 429). |
X-RateLimit-Reset | Unix-seconds timestamp when the window resets. |
Honor Retry-After and you'll be fine. The window slides — there's no fixed-minute boundary.
Monthly limits
Each API key has a per-month render quota:
| Render kind | Included per month |
|---|---|
| Image | 10,000 |
| Video | 500 |
Limits reset on the first day of each month (UTC). We're not enforcing the cap in v1 — usage is tracked per key and we'll start returning 429 rate_limited once we have signal on real traffic. If you're planning a workload that approaches these numbers, email us first so we can set you up.