UlazAI developer docs

Agent quickstart

Point-and-shoot integration for agents

Use this page when you want other websites, AI agents, or automation tools to call UlazAI directly without session cookies or CSRF handling.

Use Authorization: Bearer YOUR_ULAZAI_KEY on every request.

Installable skill + ready clients

If you use the Skills CLI, install the UlazAI skill directly from GitHub.

npx skills add https://github.com/smrht/ulazai-agent-skills --skill ulazai-point-and-shoot

If your source repo is private, use a public skill repo (recommended) or install from a local path: npx skills add /absolute/path/to/repo --skill ulazai-point-and-shoot

Why teams use this skill: low-cost endpoints for the newest models, one stable UlazAI API surface, and fast integration for white-label apps.
Download file
"""UlazAI point-and-shoot API client.

Minimal wrapper for:
- model discovery
- image generation/status/history
- video studio generation/status/history
- video studio tool endpoints
"""

from __future__ import annotations

import time
from typing import Any, Dict, Optional

import requests


class UlazAIAPIError(Exception):
    """Raised when the UlazAI API returns a non-success response."""

    def __init__(
        self,
        *,
        status_code: int,
        message: str,
        payload: Optional[Dict[str, Any]] = None,
    ) -> None:
        super().__init__(f"HTTP {status_code}: {message}")
        self.status_code = status_code
        self.message = message
        self.payload = payload or {}


class UlazAIClient:
    def __init__(
        self,
        api_key: str,
        *,
        base_url: str = "https://ulazai.com",
        timeout_seconds: int = 120,
        session: Optional[requests.Session] = None,
    ) -> None:
        if not api_key or not api_key.strip():
            raise ValueError("api_key is required")

        self.base_url = base_url.rstrip("/")
        self.timeout_seconds = timeout_seconds
        self.session = session or requests.Session()
        self.session.headers.update(
            {
                "Authorization": f"Bearer {api_key.strip()}",
                "Content-Type": "application/json",
                "Accept": "application/json",
                "User-Agent": "UlazAI-Python-Client/1.0",
            }
        )

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        json_body: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        url = f"{self.base_url}{path}"
        response = self.session.request(
            method=method,
            url=url,
            params=params,
            json=json_body,
            timeout=self.timeout_seconds,
        )

        try:
            payload = response.json()
        except ValueError:
            payload = {"success": False, "error": response.text or "Invalid JSON response"}

        if response.ok:
            return payload

        error_message = (
            str(payload.get("error") or payload.get("message") or response.reason)
            if isinstance(payload, dict)
            else response.reason
        )
        raise UlazAIAPIError(
            status_code=response.status_code,
            message=error_message,
            payload=payload if isinstance(payload, dict) else None,
        )

    # Model discovery
    def list_image_models(self) -> Dict[str, Any]:
        return self._request("GET", "/api/v1/models/image/")

    def list_video_models(self) -> Dict[str, Any]:
        return self._request("GET", "/api/v1/models/video/")

    # Images
    def generate_image(
        self,
        *,
        prompt: str,
        model: str,
        size: Optional[str] = None,
        quality: Optional[str] = None,
        google_search: Optional[bool] = None,
        extra: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        payload: Dict[str, Any] = {"prompt": prompt, "model": model}
        if size:
            payload["size"] = size
        if quality:
            payload["quality"] = quality
        if google_search is not None:
            payload.setdefault("input", {})["google_search"] = bool(google_search)
        if extra:
            payload.update(extra)
        return self._request("POST", "/api/v1/generate/", json_body=payload)

    def get_image_status(self, generation_id: str) -> Dict[str, Any]:
        return self._request("GET", f"/api/v1/generate/{generation_id}/")

    def list_image_history(self, *, page: int = 1, limit: int = 20) -> Dict[str, Any]:
        return self._request("GET", "/api/v1/generate/history/", params={"page": page, "limit": limit})

    def wait_for_image(
        self,
        generation_id: str,
        *,
        timeout_seconds: int = 300,
        poll_interval_seconds: float = 2.0,
    ) -> Dict[str, Any]:
        deadline = time.time() + timeout_seconds
        while time.time() < deadline:
            payload = self.get_image_status(generation_id)
            status_value = str(
                payload.get("status")
                or payload.get("data", {}).get("status")
                or payload.get("generation", {}).get("status")
                or ""
            ).lower()
            if status_value in {"completed", "failed"}:
                return payload
            time.sleep(poll_interval_seconds)
        raise TimeoutError(f"Image generation timed out after {timeout_seconds}s")

    # Videos
    def generate_video(
        self,
        *,
        model_slug: str,
        prompt: str,
        aspect_ratio: Optional[str] = None,
        duration_seconds: Optional[int] = None,
        quality_mode: Optional[str] = None,
        extra: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        payload: Dict[str, Any] = {"model_slug": model_slug, "prompt": prompt}
        if aspect_ratio:
            payload["aspect_ratio"] = aspect_ratio
        if duration_seconds:
            payload["duration_seconds"] = duration_seconds
        if quality_mode:
            payload["quality_mode"] = quality_mode
        if extra:
            payload.update(extra)
        return self._request("POST", "/api/v1/video-studio/generate/", json_body=payload)

    def get_video_status(self, job_id: str) -> Dict[str, Any]:
        return self._request("GET", f"/api/v1/video-studio/status/{job_id}/")

    def list_video_history(self, *, limit: int = 20) -> Dict[str, Any]:
        return self._request("GET", "/api/v1/video-studio/history/", params={"limit": limit})

    def wait_for_video(
        self,
        job_id: str,
        *,
        timeout_seconds: int = 600,
        poll_interval_seconds: float = 3.0,
    ) -> Dict[str, Any]:
        deadline = time.time() + timeout_seconds
        while time.time() < deadline:
            payload = self.get_video_status(job_id)
            job = payload.get("job", {}) if isinstance(payload, dict) else {}
            status_value = str(job.get("status") or payload.get("status") or "").lower()
            if status_value in {"completed", "failed"}:
                return payload
            time.sleep(poll_interval_seconds)
        raise TimeoutError(f"Video generation timed out after {timeout_seconds}s")

    # Video tools
    def generate_street_interview(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        return self._request(
            "POST",
            "/api/v1/video-studio/tools/street-interview/generate/",
            json_body=payload,
        )

    def generate_ugc_ad_quick(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        return self._request(
            "POST",
            "/api/v1/video-studio/tools/ugc-ad-quick/generate/",
            json_body=payload,
        )

    def generate_video_remix(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        return self._request(
            "POST",
            "/api/v1/video-studio/tools/video-remix/generate/",
            json_body=payload,
        )
/**
 * UlazAI point-and-shoot API client.
 *
 * Works in Node 18+ and modern browsers that support fetch.
 */

export class UlazAIAPIError extends Error {
  constructor(statusCode, message, payload = {}) {
    super(`HTTP ${statusCode}: ${message}`);
    this.name = "UlazAIAPIError";
    this.statusCode = statusCode;
    this.payload = payload;
  }
}

export class UlazAIClient {
  constructor({ apiKey, baseUrl = "https://ulazai.com", timeoutMs = 120000 } = {}) {
    if (!apiKey || !String(apiKey).trim()) {
      throw new Error("apiKey is required");
    }

    this.apiKey = String(apiKey).trim();
    this.baseUrl = String(baseUrl).replace(/\/+$/, "");
    this.timeoutMs = timeoutMs;
  }

  async request(method, path, { params, body } = {}) {
    const url = new URL(`${this.baseUrl}${path}`);
    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined && value !== null && value !== "") {
          url.searchParams.set(key, String(value));
        }
      });
    }

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.timeoutMs);

    let response;
    let payload;
    try {
      response = await fetch(url, {
        method,
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          "Content-Type": "application/json",
          Accept: "application/json",
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal,
      });

      const raw = await response.text();
      try {
        payload = raw ? JSON.parse(raw) : {};
      } catch {
        payload = { success: false, error: raw || "Invalid JSON response" };
      }
    } finally {
      clearTimeout(timeout);
    }

    if (response.ok) {
      return payload;
    }

    const message = payload?.error || payload?.message || response.statusText || "Request failed";
    throw new UlazAIAPIError(response.status, message, payload);
  }

  // Model discovery
  listImageModels() {
    return this.request("GET", "/api/v1/models/image/");
  }

  listVideoModels() {
    return this.request("GET", "/api/v1/models/video/");
  }

  // Images
  generateImage({ prompt, model, size, quality, googleSearch, extra = {} }) {
    const payload = { prompt, model, ...extra };
    if (size) payload.size = size;
    if (quality) payload.quality = quality;
    if (googleSearch !== undefined) {
      payload.input = { ...(payload.input || {}), google_search: Boolean(googleSearch) };
    }
    return this.request("POST", "/api/v1/generate/", { body: payload });
  }

  getImageStatus(generationId) {
    return this.request("GET", `/api/v1/generate/${generationId}/`);
  }

  listImageHistory({ page = 1, limit = 20 } = {}) {
    return this.request("GET", "/api/v1/generate/history/", { params: { page, limit } });
  }

  async waitForImage(generationId, { timeoutMs = 300000, pollMs = 2000 } = {}) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      const payload = await this.getImageStatus(generationId);
      const status = String(
        payload?.status || payload?.data?.status || payload?.generation?.status || ""
      ).toLowerCase();
      if (status === "completed" || status === "failed") {
        return payload;
      }
      await new Promise((resolve) => setTimeout(resolve, pollMs));
    }
    throw new Error(`Image generation timed out after ${timeoutMs}ms`);
  }

  // Videos
  generateVideo({
    modelSlug,
    prompt,
    aspectRatio,
    durationSeconds,
    qualityMode,
    extra = {},
  }) {
    const payload = { model_slug: modelSlug, prompt, ...extra };
    if (aspectRatio) payload.aspect_ratio = aspectRatio;
    if (durationSeconds) payload.duration_seconds = durationSeconds;
    if (qualityMode) payload.quality_mode = qualityMode;
    return this.request("POST", "/api/v1/video-studio/generate/", { body: payload });
  }

  getVideoStatus(jobId) {
    return this.request("GET", `/api/v1/video-studio/status/${jobId}/`);
  }

  listVideoHistory({ limit = 20 } = {}) {
    return this.request("GET", "/api/v1/video-studio/history/", { params: { limit } });
  }

  async waitForVideo(jobId, { timeoutMs = 600000, pollMs = 3000 } = {}) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      const payload = await this.getVideoStatus(jobId);
      const status = String(payload?.job?.status || payload?.status || "").toLowerCase();
      if (status === "completed" || status === "failed") {
        return payload;
      }
      await new Promise((resolve) => setTimeout(resolve, pollMs));
    }
    throw new Error(`Video generation timed out after ${timeoutMs}ms`);
  }

  // Video tools
  generateStreetInterview(payload) {
    return this.request("POST", "/api/v1/video-studio/tools/street-interview/generate/", {
      body: payload,
    });
  }

  generateUgcAdQuick(payload) {
    return this.request("POST", "/api/v1/video-studio/tools/ugc-ad-quick/generate/", {
      body: payload,
    });
  }

  generateVideoRemix(payload) {
    return this.request("POST", "/api/v1/video-studio/tools/video-remix/generate/", {
      body: payload,
    });
  }
}
---
name: ulazai-point-and-shoot
description: UlazAI API skill for low-cost image and video generation across newest models (Nano Banana 2, Seedream 5.0 Lite, Wan 2.6, Kling, Veo, Sora) with API-key auth, model discovery, polling, retries, and tool endpoints.
---

# UlazAI Point-and-Shoot Skill

Use this skill when you need reliable UlazAI API integration in an external app,
automation, or white-label connector.

## Why UlazAI

Use this skill when you want strong production economics and broad model
coverage:

- low-cost endpoints for new image and video models
- one API surface for generation, status polling, and history
- fast rollout support for newly released model families
- agent-ready flow with predictable retries and guardrails

## Required auth

Send this header on every request:

- `Authorization: Bearer {{ULAZAI_API_KEY}}`

Base URL:

- `https://ulazai.com`

## API workflow

Follow this order for every integration:

1. Discover supported models first.
2. Create generation job.
3. Poll status endpoint until `completed` or `failed`.
4. Return output URLs and metadata.

### Model discovery

- Images: `GET /api/v1/models/image/`
- Videos: `GET /api/v1/models/video/`

### Image generation

- Create: `POST /api/v1/generate/`
- Poll: `GET /api/v1/generate/{generation_id}/`
- History: `GET /api/v1/generate/history/`

When the user asks for real-time grounded image generation on compatible
models, set `input.google_search=true` in the image payload.

### Video Studio generation

- Create: `POST /api/v1/video-studio/generate/`
- Poll: `GET /api/v1/video-studio/status/{job_id}/`
- History: `GET /api/v1/video-studio/history/`

### Video Studio tools

- Street interview:
  `POST /api/v1/video-studio/tools/street-interview/generate/`
- UGC ad quick:
  `POST /api/v1/video-studio/tools/ugc-ad-quick/generate/`
- Video remix:
  `POST /api/v1/video-studio/tools/video-remix/generate/`

## Error behavior

- `401` or `403`: stop and request a valid API key.
- `402`: report insufficient credits.
- `429` and `5xx`: retry with exponential backoff.
- `400`: show validation error from response and let user fix input.

## Polling defaults

- Image jobs: poll every 2 seconds, timeout after 5 minutes.
- Video jobs: poll every 3 seconds, timeout after 10 minutes.

## Reusable clients in this skill

Use these files when code generation is requested:

- Python: `references/ulazai_client.py`
- JavaScript: `references/ulazai_client.js`

## Minimal cURL examples

```bash
curl -X GET https://ulazai.com/api/v1/models/image/ \
  -H "Authorization: Bearer YOUR_ULAZAI_KEY"
```

```bash
curl -X POST https://ulazai.com/api/v1/generate/ \
  -H "Authorization: Bearer YOUR_ULAZAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Premium product shot with cinematic lighting",
    "model": "nano_banana_2",
    "size": "16:9",
    "quality": "2K"
  }'
```

```bash
curl -X POST https://ulazai.com/api/v1/video-studio/generate/ \
  -H "Authorization: Bearer YOUR_ULAZAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_slug": "wan_2_6",
    "prompt": "Cinematic product teaser, smooth camera movement",
    "aspect_ratio": "16:9",
    "duration_seconds": 10,
    "quality_mode": "1080p"
  }'
```

The 60-second flow

Step Endpoint Purpose
1 /api/v1/models/image/ / /api/v1/models/video/ Discover valid model and model_slug values
2 /api/v1/generate/ or /api/v1/video-studio/generate/ Create image or video generation job
3 /api/v1/generate/{generation_id}/ or /api/v1/video-studio/status/{job_id}/ Poll until completed and read result URLs

Minimal cURL recipes

Discover image models

curl -X GET https://ulazai.com/api/v1/models/image/ \
  -H "Authorization: Bearer YOUR_ULAZAI_KEY"

Generate image (unified endpoint)

curl -X POST https://ulazai.com/api/v1/generate/ \
  -H "Authorization: Bearer YOUR_ULAZAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Premium running shoe hero shot, dramatic studio light",
    "model": "nano_banana_2",
    "size": "16:9",
    "quality": "2K"
  }'

Generate video (all video models)

curl -X POST https://ulazai.com/api/v1/video-studio/generate/ \
  -H "Authorization: Bearer YOUR_ULAZAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_slug": "wan_2_6",
    "prompt": "Cinematic product teaser with smooth dolly motion",
    "aspect_ratio": "16:9",
    "duration_seconds": 10,
    "quality_mode": "1080p"
  }'

Poll video status

curl -X GET https://ulazai.com/api/v1/video-studio/status/YOUR_JOB_ID/ \
  -H "Authorization: Bearer YOUR_ULAZAI_KEY"

Copy-paste agent skill

Paste this block in your own agent system prompt or skill config.

# UlazAI Point-and-Shoot Skill

Base URL: https://ulazai.com
Auth: Authorization: Bearer 

1) Model discovery
- Images: GET /api/v1/models/image/
- Videos: GET /api/v1/models/video/

2) Image tasks
- Create: POST /api/v1/generate/
- Poll:   GET  /api/v1/generate/{generation_id}/
- History: GET /api/v1/generate/history/

3) Video tasks (all video model_slugs)
- Create: POST /api/v1/video-studio/generate/
- Poll:   GET  /api/v1/video-studio/status/{job_id}/
- History: GET /api/v1/video-studio/history/

4) Tool flows (video)
- Street interview: POST /api/v1/video-studio/tools/street-interview/generate/
- UGC ad quick:     POST /api/v1/video-studio/tools/ugc-ad-quick/generate/
- Video remix:      POST /api/v1/video-studio/tools/video-remix/generate/

5) Guardrails
- Always discover models first and validate requested slug.
- Retry 429/5xx with backoff.
- Stop immediately on 401/403 and request a valid key.
- On 402, report insufficient credits clearly.

Recommended architecture for white-label apps

  1. Keep UlazAI keys server-side only. Never expose raw keys in public frontend code.
  2. Create one backend adapter with a single request format for your own app.
  3. Map your internal model names to UlazAI model identifiers from discovery endpoints.
  4. Persist generation_id and job_id so polling and retries are idempotent.
  5. Cache model capability responses for 5-15 minutes to reduce metadata traffic.