React Example
React Example (Loader + Polling)
This example shows an end-to-end integration flow in React:
- Request an access token (
POST /oauth/token) - Create a try-on job (
POST /v1/mirrago/generate-image) - Show a loader while processing
- Poll for completion (
GET /v1/mirrago/job/{job_id}) - Render the final
result_url
Security note: Keep
client_secreton the server whenever possible. If you must run this demo in a browser, use it only for internal testing and restrict the key.
Prerequisites
- Node.js 18+
- A React app (Vite or Next.js)
- Mirrago
client_idandclient_secret
Environment Variables
Vite
Create a .env file:
VITE_MIRRAGO_BASE_URL="https://api.mirrago.com"
VITE_MIRRAGO_CLIENT_ID="YOUR_CLIENT_ID"
VITE_MIRRAGO_CLIENT_SECRET="YOUR_CLIENT_SECRET"
Next.js
Create a .env.local file:
NEXT_PUBLIC_MIRRAGO_BASE_URL="https://api.mirrago.com"
NEXT_PUBLIC_MIRRAGO_CLIENT_ID="YOUR_CLIENT_ID"
NEXT_PUBLIC_MIRRAGO_CLIENT_SECRET="YOUR_CLIENT_SECRET"
React Component (Vite-friendly)
Create src/TryOnDemo.tsx:
import React, { useMemo, useState } from "react";
type JobStatus = "queued" | "pending" | "completed" | "failed";
type TokenResponse = {
access_token: string;
expires_in: number;
token_type: "Bearer" | string;
};
type GenerateImageResponse = {
job_id: string;
status: JobStatus;
};
type JobStatusResponse = {
job_id: string;
status: JobStatus;
result_image_url?: string;
error?: string;
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export default function TryOnDemo() {
const baseUrl = import.meta.env.VITE_MIRRAGO_BASE_URL ?? "https://api.mirrago.com";
const clientId = import.meta.env.VITE_MIRRAGO_CLIENT_ID;
const clientSecret = import.meta.env.VITE_MIRRAGO_CLIENT_SECRET;
// Files (multipart/form-data)
const [modelFile, setModelFile] = useState<File | null>(null);
const [garmentFile, setGarmentFile] = useState<File | null>(null);
// Optional fields
const [background, setBackground] = useState<string>("studio");
const [username, setUsername] = useState<string>("");
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<JobStatus | null>(null);
const [jobId, setJobId] = useState<string | null>(null);
const [resultUrl, setResultUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const canRun = useMemo(() => {
return Boolean(clientId && clientSecret && modelFile && garmentFile);
}, [clientId, clientSecret, modelFile, garmentFile]);
async function getAccessToken(): Promise<string> {
const res = await fetch(`${baseUrl}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
grant_type: "client_credentials",
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Token request failed (${res.status}): ${text}`);
}
const data = (await res.json()) as TokenResponse;
return data.access_token;
}
async function createTryOnJob(token: string): Promise<GenerateImageResponse> {
if (!modelFile || !garmentFile) {
throw new Error("Please select both model and garment images.");
}
const form = new FormData();
// IMPORTANT: field names must match backend
form.append("model_image", modelFile);
form.append("garment_image", garmentFile);
// Optional fields (only append if present)
if (background) form.append("background", background);
if (username) form.append("username", username);
const res = await fetch(`${baseUrl}/v1/mirrago/generate-image`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
// Do NOT set Content-Type manually for multipart; browser sets boundary.
},
body: form,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Generate-image request failed (${res.status}): ${text}`);
}
return (await res.json()) as GenerateImageResponse;
}
async function getJobStatus(token: string, id: string): Promise<JobStatusResponse> {
const res = await fetch(`${baseUrl}/v1/mirrago/job/${encodeURIComponent(id)}`, {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const text = await res.text();
throw new Error(`getJobStatus failed (${res.status}): ${text}`);
}
return (await res.json()) as JobStatusResponse;
}
async function run() {
setLoading(true);
setError(null);
setResultUrl(null);
setJobId(null);
setStatus("queued");
try {
const token = await getAccessToken();
const created = await createTryOnJob(token);
setJobId(created.job_id);
setStatus(created.status);
// Poll until completed or failed
const pollEveryMs = 1000;
const timeoutMs = 30000;
const maxAttempts = Math.ceil(timeoutMs / pollEveryMs);
for (let i = 0; i < maxAttempts; i++) {
const s = await getJobStatus(token, created.job_id);
setStatus(s.status);
if (s.status === "completed") {
setResultUrl(s.result_image_url ?? null);
return;
}
if (s.status === "failed") {
setError(s.error ?? "Generation failed.");
return;
}
await sleep(pollEveryMs);
}
setError("Timed out waiting for result. Please try again.");
} catch (e: any) {
setError(e?.message ?? "Something went wrong.");
} finally {
setLoading(false);
}
}
return (
<div style={{ maxWidth: 900, margin: "40px auto", fontFamily: "system-ui, -apple-system, Segoe UI, Roboto" }}>
<h1 style={{ marginBottom: 6 }}>Mirrago Try-On (React Demo)</h1>
<p style={{ marginTop: 0, opacity: 0.85 }}>
Uploads files to <code>/v1/mirrago/generate-image</code> and polls <code>/v1/getJobStatus</code> until ready.
</p>
<div style={{ display: "grid", gap: 12, marginTop: 16 }}>
<label style={{ display: "grid", gap: 6 }}>
<span style={{ fontWeight: 600 }}>Model Image (JPG/PNG)</span>
<input
type="file"
accept="image/*"
onChange={(e) => setModelFile(e.target.files?.[0] ?? null)}
/>
{modelFile && <small style={{ opacity: 0.85 }}>{modelFile.name}</small>}
</label>
<label style={{ display: "grid", gap: 6 }}>
<span style={{ fontWeight: 600 }}>Garment Image (JPG/PNG)</span>
<input
type="file"
accept="image/*"
onChange={(e) => setGarmentFile(e.target.files?.[0] ?? null)}
/>
{garmentFile && <small style={{ opacity: 0.85 }}>{garmentFile.name}</small>}
</label>
<label style={{ display: "grid", gap: 6 }}>
<span style={{ fontWeight: 600 }}>Background (optional)</span>
<input
value={background}
onChange={(e) => setBackground(e.target.value)}
placeholder="studio"
style={{ padding: 12, borderRadius: 10, border: "1px solid #ddd" }}
/>
</label>
<label style={{ display: "grid", gap: 6 }}>
<span style={{ fontWeight: 600 }}>Username (optional)</span>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="bittu"
style={{ padding: 12, borderRadius: 10, border: "1px solid #ddd" }}
/>
</label>
<button
onClick={run}
disabled={!canRun || loading}
style={{
padding: "12px 14px",
borderRadius: 12,
border: "1px solid #ddd",
fontWeight: 700,
cursor: !canRun || loading ? "not-allowed" : "pointer",
}}
>
{loading ? "Generating..." : "Generate Try-On"}
</button>
{!canRun && (
<div style={{ opacity: 0.85 }}>
Set your env vars and select both images.
</div>
)}
</div>
<div style={{ marginTop: 22, padding: 16, border: "1px solid #eee", borderRadius: 14 }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<strong>Status:</strong>
<span>{status ?? "-"}</span>
{loading && <Spinner />}
</div>
{jobId && (
<div style={{ marginTop: 10, opacity: 0.9 }}>
<strong>Job ID:</strong> <code>{jobId}</code>
</div>
)}
{error && (
<div style={{ marginTop: 12, color: "crimson" }}>
<strong>Error:</strong> {error}
</div>
)}
</div>
{resultUrl && (
<div style={{ marginTop: 18 }}>
<h2 style={{ marginBottom: 10 }}>Result</h2>
<img
src={resultUrl}
alt="Try-on result"
style={{ width: "100%", borderRadius: 16, border: "1px solid #eee" }}
/>
</div>
)}
</div>
);
}
function Spinner() {
return (
<span
aria-label="loading"
style={{
width: 14,
height: 14,
borderRadius: "50%",
border: "2px solid #999",
borderTopColor: "transparent",
display: "inline-block",
animation: "spin 0.8s linear infinite",
}}
>
<style>
{`
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}
</style>
</span>
);
}
Notes and Best Practices
Polling
- Recommended: 1 second
- Timeout: 30 seconds (adjust for your use case)
- Stop polling when
statusiscompletedorfailed
Production recommendation (important)
For production apps, do not request tokens in the browser. Instead:
- Create a small backend endpoint in your system that calls
POST /oauth/token - Your frontend calls your backend (never exposing
client_secret)
Next
- Integration Notes: image guidelines, retries, and scaling patterns
- UI Example: add progress messaging and retry controls
Dev Mirrago