Your first generation

Create an image through the same job API used by the Vydra workspace.

1. Create an API key

Sign in and open API Keys. Store the key when it is shown. Keep it in your server environment or secret manager; do not put it in browser JavaScript. Studio and library access requires jobs:read and jobs:write, or a matching wildcard scope.

export VYDRA_API_KEY="YOUR_API_KEY"

Agents can also use POST /auth/bot-register; see authentication and OpenClaw/MCP setup.

2. Select a model and check credits

curl https://vydra.ai/api/v1/creator/models \
  -H "Authorization: Bearer $VYDRA_API_KEY"

curl https://vydra.ai/api/v1/account \
  -H "Authorization: Bearer $VYDRA_API_KEY"

The creator catalog returns {"data":[…]}. Its estimates use default settings; changing resolution, duration, or script length changes the charge. This example uses Nano Banana at 1K for 8 credits. See model settings and prices.

3. Submit a job

curl -X POST https://vydra.ai/api/v1/jobs \
  -H "Authorization: Bearer $VYDRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"workflow":"generate_image","input":{"model":"nano-banana","prompt":"An ivory dragon above a stormy sea, cinematic sunlight","resolution":"1K","aspect_ratio":"16:9"}}'

A successful canonical request returns HTTP 201 with top-level id, status, and creditsCharged. It is not wrapped in data. Copy the ID, then request its result:

curl https://vydra.ai/api/v1/jobs/JOB_ID \
  -H "Authorization: Bearer $VYDRA_API_KEY"

Poll every 4–5 seconds while status is pending or running. On completion, read result.imageUrl, result.videoUrl, result.audioUrl, or result.text according to the workflow. On failure, inspect error and refund fields; on cancellation, stop polling.

Server-side JavaScript example

const base = "https://vydra.ai/api/v1";
const headers = { Authorization: `Bearer ${process.env.VYDRA_API_KEY}` };
async function request(path, options = {}) {
  const response = await fetch(base + path, {
    ...options,
    headers: { ...headers, ...options.headers },
    signal: AbortSignal.timeout(300_000),
  });
  const body = await response.json();
  if (!response.ok) throw new Error(JSON.stringify(body));
  return body;
}
const created = await request("/jobs", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    workflow: "generate_image",
    input: { model: "nano-banana", resolution: "1K",
      prompt: "An ivory dragon above a stormy sea", aspect_ratio: "16:9" },
  }),
});
console.log("Save this job ID:", created.id);
const deadline = Date.now() + 10 * 60_000;
let result;
while (Date.now() < deadline) {
  const job = await request(`/jobs/${created.id}`);
  if (job.status === "completed") { result = job.result; break; }
  if (["failed", "cancelled"].includes(job.status)) {
    throw new Error(JSON.stringify({ status: job.status, error: job.error,
      creditsRefunded: job.creditsRefunded, refundStatus: job.refundStatus }));
  }
  await new Promise(resolve => setTimeout(resolve, 5000));
}
if (!result) throw new Error("Polling timed out; resume with the saved job ID.");
console.log(result.imageUrl);

Retry reads, not paid submissions

A network timeout does not prove that a generation failed to start. Ordinary POST /jobs does not implement Studio run idempotency. Do not automatically resubmit it; check job history first. A polling deadline only stops your client, not the server job.

Next: run a saved Studio workflow, save references, or generate speech.