1. SDK
  2. Stateless Calls

Choose a workflow:

WorkflowHow it works
Conversational image tools / video toolsEnable image_generation="auto" or video_generation="auto". Your chat model decides when to generate.
Stateless generationSet operation="generate_image" or "generate_video". Your prompt and explicit inputs go to the selected media model, without an intermediary chat-model call.

Both use POST /threads/messages, with the same model/provider and configuration names. Stateless generation currently supports openrouter.

  • Stateless Image API: text-to-image, editing, multiple references, and image settings.
  • Stateless Video API: text-to-video, first/last frames, video editing, every reference-modality combination, and video settings.

“Stateless” means no pre-created assistant/thread and no conversational context required. The API still stores messages and media metadata and returns thread_id and assistant_id. File contents live in S3. These examples require backend and SDK versions exposing operation; the SDK source must be released before installed packages gain this feature.

Start with one call

import asyncio
import os
from backboard import BackboardClient

async def main():
    async with BackboardClient(
        api_key=os.environ["BACKBOARD_API_KEY"], timeout=1900,
    ) as client:
        result = await client.send_message(
            "A blue robot watering plants.",
            operation="generate_image",
            image_model_provider="openrouter",
            image_model_name="google/gemini-3.1-flash-image",
            image_config={"resolution": "1K"},
        )
        for media in result.generated_media:
            print(media["url"])

asyncio.run(main())

Install backboard-sdk with pip or npm. Python client timeouts are in seconds; JavaScript timeouts are in milliseconds. Run the Python recipes on the following pages inside the async with ... as client block above.

Shared request fields

FieldUse
operationRequired for stateless generation: generate_image or generate_video. Omit it for normal chat.
contentRequired text prompt, including when files are supplied.
image_model_provider, image_model_name, image_configImage model and optional settings. Use only for generate_image.
video_model_provider, video_model_name, video_configVideo model and optional settings. Use only for generate_video.
streamfalse by default. Set true for SSE with keepalives and media events.
metadataOptional JSON object stored with the user message.
thread_id, assistant_idOptional existing context identifiers. Omit both for a one-shot call. They do not make stateless generation consume chat history.

Do not pass a chat model (llm_provider / model_name), tools, voice, system prompts, thinking, search, or enabled memory controls. The generation tool switches (image_generation / video_generation) are not needed. Put provider routing inside the selected media config, not the chat-level openrouter field.

Files: explicit roles, not filenames in the prompt

FieldRole
input_imageImage-edit input or first video frame.
input_last_frameLast video frame; requires input_image.
input_videoSource video for video editing/conditioning, if supported.
input_referencesGeneral reference files. Accepted modalities depend on the output model.

SDK values are local file paths, not URLs or document IDs. Raw HTTP uses multipart file fields with these exact names; repeat input_references for multiple files. Serialize the config object as a JSON string in multipart requests. Do not use the conversational files field here.

Upload order and filenames do not assign roles. A first frame and a general image reference are different inputs. See the video combination rules.

Read results or stream them

Non-streaming HTTP responses contain generated_media: each item includes document_id, media_type, mime_type, url, and file_size_bytes. Python exposes result.generated_media; JavaScript exposes message.generatedMedia in result.messages.

stream = await client.send_message(
    "A blue robot watering plants.",
    operation="generate_image",
    image_model_provider="openrouter",
    image_model_name="google/gemini-3.1-flash-image",
    stream=True,
)
async for event in stream:
    if event["type"] == "media_generated":
        print(event["media"]["url"])
    elif event["type"] == "error":
        raise RuntimeError(event["error"])
    elif event["type"] == "run_ended":
        print(event["status"], event["thread_id"])

For JavaScript, pass stream: true and consume the result with for await. Event names and fields match Python. message_received includes the created thread/assistant IDs; successful streams end with run_ended. HTTP 200 alone is not proof that a stream succeeded.

Limits, errors, and lifecycle

OutcomeWhat to do
400: invalid input or configurationCheck the model, role combination, and config values. Do not silently drop inputs.
402: insufficient creditsFund the appropriate billing balance before retrying.
413: upload too largeReduce inputs to the deployment’s per-file and aggregate limits. Provider limits can be lower.
503: capabilities unavailableModel discovery failed before generation.
502 or a streamed errorGeneration/storage failed. Do not assume the provider incurred no cost.

Generation is billed. Allow a long timeout and prefer streaming for videos. Do not automatically retry a timed-out generation: its upstream job may still be running. Disconnecting does not cancel stateless generation; the request continues to finish storage and billing. Active stateless generation runs return 409 from the chat cancellation endpoint.

This interface waits for completion; it is not a separate submit-and-poll API or a durable background queue. Reuse a returned thread_id for organization or a later normal chat request; each subsequent stateless generation still needs its own prompt and explicit inputs.