Gemini Omni
api developer integration tutorial

Calling Gemini Omni from Your App: A Developer's Guide

Gemini Omni Team · · Updated June 5, 2026

The Gemini Omni REST API is available on Pro and Premium plans. It lets you submit generation jobs, poll for results, and receive webhook notifications all from your own application without touching the browser Playground. This tutorial walks you through the full integration: authentication, generating a video, handling the async result, and dealing with errors.

If you prefer to follow along in the browser first, the Playground is the fastest way to understand what you’re building before you write any code.

Prerequisites

  • A Pro or Premium Gemini Omni account
  • An API key (from /api-keys)
  • Basic familiarity with REST APIs and async patterns

1. Get your API key

API keys are created in the /api-keys dashboard page. They are only shown once at creation time copy it immediately. The format is gomni_ followed by 64 hex characters.

Store your key securely. Never commit it to version control. Use environment variables or a secrets manager:

# .env
GEMINI_OMNI_API_KEY=gomni_your_key_here

All requests are authenticated with a Bearer token in the Authorization header:

Authorization: Bearer gomni_your_key_here

2. Submit a generation job

All generation types use the same endpoint: POST https://googlegeminiomni.com/api/v1/generate

Text-to-video

curl -X POST https://googlegeminiomni.com/api/v1/generate \
  -H "Authorization: Bearer $GEMINI_OMNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-omni-video",
    "prompt": "A lone lighthouse on a rocky cliff, storm waves crashing below, dark clouds, cinematic 4K wide shot, slow push forward",
    "resolution": "1080p",
    "duration": 8
  }'

The API returns immediately with a job ID and accepted status:

{
  "jobId": "job_abc123",
  "status": "accepted",
  "creditCost": 150,
  "estimatedSeconds": 60
}

Image-to-video

Supply the first frame as imageUrl. The model animates from that image:

{
  "model": "gemini-omni-video",
  "prompt": "The lighthouse beam sweeping through the storm, waves intensifying",
  "imageUrl": "https://your-cdn.com/lighthouse.jpg",
  "resolution": "1080p",
  "duration": 8
}

Character video

Upload a reference photo first via POST /api/v1/upload, then reference the returned URL:

{
  "model": "gemini-omni-character",
  "prompt": "The character presenting to camera in a modern boardroom, confident and professional",
  "referenceUrl": "https://your-cdn.com/reference-face.jpg",
  "resolution": "1080p",
  "duration": 8
}

AI voice (text-to-speech)

{
  "model": "gemini-omni-audio",
  "script": "Welcome to your daily briefing. Here are the top three priorities for today.",
  "language": "en-US",
  "voiceProfile": "professional-male"
}

Lip-sync

Attach a character video to a voice job for synchronized lip movement:

{
  "model": "gemini-omni-audio",
  "script": "Welcome to your daily briefing.",
  "language": "en-US",
  "voiceProfile": "professional-male",
  "videoUrl": "https://your-cdn.com/character-clip.mp4"
}

3. Poll for results

Generation is asynchronous. After submitting, poll GET /api/v1/jobs/{jobId} until the status is done or failed.

async function pollJob(jobId, apiKey) {
  const maxAttempts = 60;
  const intervalMs = 3000;

  for (let i = 0; i < maxAttempts; i++) {
    const res = await fetch(`https://googlegeminiomni.com/api/v1/jobs/${jobId}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    const job = await res.json();

    if (job.status === 'done') {
      return job.outputUrl;
    }

    if (job.status === 'failed') {
      throw new Error(`Job failed: ${job.error}`);
    }

    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }

  throw new Error('Job timed out after 3 minutes');
}

Status values:

  • accepted received, queued
  • generating model is running
  • done complete, outputUrl is available
  • failed model error, no credits consumed

outputUrl is a signed R2 URL valid for 7 days. Download the file to your own storage before it expires.

Polling works but wastes requests. In production, use webhooks: register a callback URL on the job submission, and the platform will POST to it when the job completes.

Register a callback URL

{
  "model": "gemini-omni-video",
  "prompt": "...",
  "resolution": "1080p",
  "duration": 8,
  "callbackUrl": "https://your-app.com/webhooks/gemini-omni"
}

Webhook payload

{
  "event": "job.completed",
  "jobId": "job_abc123",
  "status": "done",
  "outputUrl": "https://r2.googlegeminiomni.com/omni/usr_xyz/job_abc123/output.mp4",
  "creditCost": 150,
  "model": "gemini-omni-video",
  "timestamp": "2026-05-15T14:23:00Z"
}

Failed jobs send:

{
  "event": "job.failed",
  "jobId": "job_abc123",
  "status": "failed",
  "error": "Content policy: prompt contains restricted subject matter",
  "creditCost": 0,
  "timestamp": "2026-05-15T14:23:00Z"
}

Verify the webhook signature

Every webhook request includes an X-Gemini-Signature header an HMAC-SHA256 of the raw request body, keyed with your webhook secret.

import crypto from 'crypto';

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your webhook handler (Express example):
app.post('/webhooks/gemini-omni', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-gemini-signature'];
  const secret = process.env.GEMINI_OMNI_WEBHOOK_SECRET;

  if (!verifyWebhook(req.body, sig, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const payload = JSON.parse(req.body.toString());

  if (payload.event === 'job.completed') {
    // download payload.outputUrl and store in your own storage
    queueDownload(payload.jobId, payload.outputUrl);
  }

  res.status(200).json({ received: true });
});

Always return 200 quickly. If your handler takes too long, the platform will retry (3 retries: 30 seconds, 5 minutes, 30 minutes). Offload actual processing to a queue.

5. Handle errors

The API uses standard HTTP status codes. Your integration should handle:

CodeMeaningAction
400Invalid request bad prompt, unknown model, missing fieldFix the request body
401Invalid or missing API keyCheck key, check Authorization header format
402Insufficient creditsTop up credits or upgrade plan
403Action not permitted API not available on Starter planUpgrade to Pro/Premium
404Job not foundCheck job ID
429Rate limit exceededBack off and retry (see below)
500Internal server errorRetry with exponential backoff
503Service unavailableRetry after delay

Rate limits by plan

PlanRequests/minuteRequests/hourConcurrent jobs
Pro302005
Premium6050010

Retry logic

async function submitWithRetry(payload, apiKey, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch('https://googlegeminiomni.com/api/v1/generate', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });

    if (res.ok) return res.json();

    const status = res.status;

    // Don't retry client errors (except 429)
    if (status >= 400 && status < 500 && status !== 429) {
      throw new Error(`Client error ${status}: ${await res.text()}`);
    }

    if (attempt < maxRetries) {
      const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
      await new Promise((r) => setTimeout(r, delay));
    }
  }

  throw new Error('Max retries exceeded');
}

6. Fan out to CDN

outputUrl is a time-limited signed URL. For production, download and re-host immediately:

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

async function ingestToYourCDN(jobId, outputUrl) {
  const response = await fetch(outputUrl);
  const buffer = await response.arrayBuffer();

  const s3 = new S3Client({ region: 'us-east-1' });

  await s3.send(new PutObjectCommand({
    Bucket: process.env.YOUR_BUCKET,
    Key: `videos/${jobId}.mp4`,
    Body: Buffer.from(buffer),
    ContentType: 'video/mp4',
  }));

  return `https://cdn.yourapp.com/videos/${jobId}.mp4`;
}

7. Full end-to-end example (Node.js)

import 'dotenv/config';

const API_KEY = process.env.GEMINI_OMNI_API_KEY;
const BASE_URL = 'https://googlegeminiomni.com';

async function generateVideo(prompt, resolution = '1080p', duration = 8) {
  // Submit
  const submitRes = await fetch(`${BASE_URL}/api/v1/generate`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ model: 'gemini-omni-video', prompt, resolution, duration }),
  });

  if (!submitRes.ok) {
    throw new Error(`Submit failed: ${await submitRes.text()}`);
  }

  const { jobId } = await submitRes.json();
  console.log(`Job submitted: ${jobId}`);

  // Poll
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 3000));

    const pollRes = await fetch(`${BASE_URL}/api/v1/jobs/${jobId}`, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    const job = await pollRes.json();

    console.log(`Status: ${job.status}`);

    if (job.status === 'done') {
      console.log(`Output: ${job.outputUrl}`);
      return job.outputUrl;
    }

    if (job.status === 'failed') {
      throw new Error(`Job failed: ${job.error}`);
    }
  }

  throw new Error('Timeout');
}

// Example usage
generateVideo(
  'A lone lighthouse on a rocky cliff, storm waves crashing below, cinematic 4K wide shot, slow push forward',
  '1080p',
  8
)
  .then((url) => console.log('Done:', url))
  .catch(console.error);

8. Bulk generation pattern

For batch workloads generating 50+ clips with different prompts use a queue to respect rate limits:

async function bulkGenerate(prompts, apiKey, concurrency = 5) {
  const results = [];
  const queue = [...prompts];
  const inFlight = new Set();

  while (queue.length > 0 || inFlight.size > 0) {
    while (queue.length > 0 && inFlight.size < concurrency) {
      const prompt = queue.shift();
      const promise = generateVideo(prompt, '1080p', 8)
        .then((url) => results.push({ prompt, url, status: 'done' }))
        .catch((err) => results.push({ prompt, error: err.message, status: 'failed' }))
        .finally(() => inFlight.delete(promise));

      inFlight.add(promise);
    }

    if (inFlight.size > 0) {
      await Promise.race(inFlight);
    }
  }

  return results;
}

This respects the 5-concurrent-job limit on Pro. Adjust concurrency to match your plan.

API reference

Full documentation is in the developer docs:

Try it in the Playground first

If you’re new to the API, the best way to understand what prompts produce what output is to experiment in the Playground before writing integration code. The Playground uses the same models, same resolution settings, and same credit costs it’s the same API under the hood.

When your prompts are dialed in from Playground testing, moving them to an API integration is straightforward.


Related:

Ready to generate your first video?

Try the Playground no configuration required.

Open Playground →