router.Nyuro.ai

Onboarding

Everything an application needs to integrate with Nyuro end to end — one key, one base URL, and the three request shapes for chat, embeddings, and media generation.

This is the integration guide for a team wiring a product into Nyuro for the first time. It covers the whole path: create a key, point at the right host, then the three surfaces — chat, embeddings, and media generation.

Chat and embeddings are synchronous and OpenAI-shaped. Media is asynchronous and Nyuro-shaped — a job you submit and poll. That difference is the single biggest source of failed first integrations, so it is called out at every step.

One host for everything

Every surface lives on https://api.routing.nyuro.ai. There is no separate media host, no regional variant, and no *.run.app address. If you are holding a URL that ends in run.app, it came from an internal build and should be replaced with the host above.

Create a key — and check the scopes

Sign in, then go to Settings → API Keys → Create key. The plaintext key is shown once; copy it immediately.

A current key looks like:

nyu_live_XXXXXXXXXXXXXXXXXXXXXX

Scopes are per capability — media is NOT on by default

The create form pre-checks chat, embeddings and models.read. media is deliberately left unchecked: media generation bills per output, and a single video costs dollars where a chat call costs a fraction of a cent.

If you intend to generate images, video, speech, or avatars, tick Media when you create the key. A key without it returns 403, not 401:

{"detail": "api key missing required scope 'media'"}
ScopeUnlocks
chatPOST /v1/chat/completions
embeddingsPOST /v1/embeddings
models.readGET /v1/models
mediaeverything under /api/v1/media/*

Treat the key like a password: server-side only, never in client-side code, never committed. Put it in an environment variable.

Older keys beginning neu_live_ still work — but migrate

Keys minted under the legacy neu_live_ prefix remain valid and are accepted everywhere nyu_live_ is. Use nyu_live_ for anything new: the legacy prefix is supported for backwards compatibility only and may be withdrawn in a future release. Rotating is a two-minute job — mint a new key, swap the environment variable, revoke the old one — and is worth doing before it is forced.

Learn the two path prefixes

This is the part that costs people an afternoon. Nyuro exposes two path families on the same host, and they are not interchangeable.

PrefixWhat lives thereShape
/v1/…chat, embeddings, modelsOpenAI-compatible
/api/v1/…media, keys, budgets, governance, collectionsNyuro platform API

/v1 is an OpenAI-compatibility alias and carries only chat, embeddings and the model catalogue. Media is never mounted there — POST /v1/media/jobs returns 404 no matter how correct the body is.

Include /v1 for an SDK, omit it for raw HTTP

The OpenAI SDK appends the path itself, so its base_url ends in /v1. A raw HTTP client writes the full path, so its base URL must not. Doing both yields /v1/v1/chat/completions and a bare 404 that reads like an outage.

ClientBase URL
OpenAI SDKhttps://api.routing.nyuro.ai/v1
curl / fetchhttps://api.routing.nyuro.ai
Media (always raw HTTP)https://api.routing.nyuro.ai

Chat — synchronous, OpenAI-compatible

Point any OpenAI client at the gateway and name a model, or use "auto" to let Nyuro choose per request.

curl https://api.routing.nyuro.ai/v1/chat/completions \
  -H "Authorization: Bearer $NYURO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "auto",
    "messages": [{"role": "user", "content": "Summarise this ticket in one line."}]
  }'
# pip install openai
from openai import OpenAI
import os

client = OpenAI(
    base_url="https://api.routing.nyuro.ai/v1",
    api_key=os.environ["NYURO_API_KEY"],
)

resp = client.chat.completions.create(
    model="auto",
    messages=[{"role": "user", "content": "Summarise this ticket in one line."}],
)
print(resp.choices[0].message.content)
print("served by:", resp.model)   # ← always check this
// npm i openai
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.routing.nyuro.ai/v1",
  apiKey: process.env.NYURO_API_KEY!,
});

const resp = await client.chat.completions.create({
  model: "auto",
  messages: [{ role: "user", content: "Summarise this ticket in one line." }],
});
console.log(resp.choices[0].message.content);
console.log("served by:", resp.model); // ← always check this

Streaming works exactly as it does with OpenAI — set "stream": true and read server-sent events.

Which model you name changes the fallback behaviour, and this is deliberate:

  • auto (or a strategy: / industry: selector) means you have delegated the choice, so Nyuro may cross vendors to keep you answered.
  • A named modelgpt-4o, claude-3-5-sonnet, gemini-flash-latest — falls back only within its own vendor, then fails loudly. gpt-4o degrades to gpt-4o-mini and otherwise errors; it will never silently answer as Claude.

Read the model field on the response, not just the status

A 200 tells you a model answered — not which one. When you have named a specific model, assert on resp.model in your integration tests. It is the only way to catch a silent substitution.

Call GET /v1/models for the live routable list rather than hard-coding one; the catalogue is curated and changes.

Embeddings — synchronous, and deliberately unforgiving

curl https://api.routing.nyuro.ai/v1/embeddings \
  -H "Authorization: Bearer $NYURO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen/Qwen3-Embedding-0.6B",
    "input": ["first passage", "second passage"]
  }'
ModelQwen/Qwen3-Embedding-0.6B (self-hosted)
Dimensions1024
Fallbacknone, by design

Embeddings never fall back — and that is the feature

A chat response from the wrong model costs you one bad answer. An embedding vector from the wrong model gets written into your vector index, where it is silently wrong forever and mixes incompatible vector spaces. So the embedding chain is empty: if the model cannot serve, the call fails rather than quietly returning vectors from something else. Handle the error; do not retry against a different model.

First call after idle can take minutes

The embedding model scales to zero. A cold start can exceed 200 seconds and looks exactly like an outage. Set a generous client timeout (180s+) on the first call rather than treating it as a failure.

Media — asynchronous jobs, not a request/response call

Media generation does not return your asset. It returns a job, which you poll. A caller who treats the first response as the finished result reads an empty asset list every time.

1. Submit the job

curl https://api.routing.nyuro.ai/api/v1/media/jobs \
  -H "Authorization: Bearer $NYURO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "operation": "text_to_image",
    "model": "nyuro-image-fast",
    "params": {"prompt": "a red bicycle leaning on a white wall"},
    "inputs": {}
  }'

The body has four fields: operation, model, params (generation settings), and inputs (source media).

inputs are URLs or data URIs — never raw bytes

Every value in inputs must be a string: a publicly fetchable URL or a data: URI. This keeps a request plain JSON — there is no multipart upload endpoint. A non-string value returns 422.

The response is 202 Accepted, not 200, carrying a non-terminal job:

{
  "id": "5f0c…",
  "object": "media.job",
  "status": "queued",
  "operation": "text_to_image",
  "model": "nyuro-image-fast",
  "assets": [],
  "error": null,
  "timings": {"queue_ms": null, "execution_ms": null}
}

2. Poll until terminal

curl https://api.routing.nyuro.ai/api/v1/media/jobs/5f0c… \
  -H "Authorization: Bearer $NYURO_API_KEY"

status moves through queuedrunningcompleted | failed | cancelled. Poll every few seconds; images land in seconds, video in about a minute. Assets appear only once status is completed:

{
  "status": "completed",
  "assets": [{
    "url": "/api/v1/media/assets/9a41…",
    "mime_type": "image/jpeg",
    "size_bytes": 184320,
    "width": 1024, "height": 1024,
    "duration_seconds": null,
    "thumbnail_url": null,
    "stored": true
  }],
  "timings": {"queue_ms": 420, "execution_ms": 3180}
}

3. Fetch the asset

asset.url is a RELATIVE path and needs your key

Prepend the base host — https://api.routing.nyuro.ai/api/v1/media/assets/… — and send the same Authorization header. The route authorises you, then 302-redirects to a short-lived signed URL. Follow redirects (curl -L; fetch does it by default).

Do not cache or store the redirect target. It expires, and it is a bearer URL for your content. Re-request the asset route whenever you need the bytes; it mints a fresh link each time.

"stored": true means Nyuro holds a durable copy. false means the asset is still only at the provider behind an expiring link — fetch it promptly.

Stored assets carry a 90-day retention stamp by default, fixed at the moment the bytes land. Treat Nyuro as the delivery path, not your system of record: if an asset matters to your product long-term, copy it into your own storage.

Available models

Call GET /api/v1/media/models for the live list — each entry carries an available flag. At time of writing:

ModelOperationsOutput
nyuro-image-fasttext_to_imageimage/jpeg
nyuro-image-editimage_to_image, image_editimage/jpeg
nyuro-videotext_to_videovideo/mp4
nyuro-avatarlip_sync, talking_avatarvideo/mp4
nyuro-speechtext_to_speech, voice_cloneaudio/wav

Two models are registered but not servable

nyuro-image (quality image) and nyuro-video-from-image (image-to-video) appear in the catalogue with "available": false and return 501 if you dispatch to them. They are listed so the capability filter is complete, not because they can be served — an upstream availability problem, not a missing parameter on your side. Filter on available rather than assuming every catalogue entry is routable.

Media has no speech-to-text, music, upscaling, or background-removal model, and no image-understanding model — every "image-to-…" operation above is generation. For image understanding, send the image to a vision-capable chat model instead.

Media costs real money per output

Video is roughly $0.50 per generation, avatars around $0.25 — not fractions of a cent. Put a budget cap on any key that carries the media scope, and prefer nyuro-image-fast while you are still developing.

Handle the errors that actually happen

StatusMeansDo this
401key missing, malformed, or revokedcheck the Authorization: Bearer header
403key lacks the scopeadd the scope — media especially, it is not granted by default
400invalid model namethe catalogue is curated; call GET /v1/models
404 on mediawrong prefixmedia is /api/v1/media/…, never /v1/media/…
422malformed bodyoperation and model are required; inputs values must be strings
501model registered but not servablepick one with "available": true
202job accepted, not finishedpoll the job — do not read assets yet

Check what came back, not only that something came back

Most integration bugs here are success-shaped: a 200 from a different model than you named, a 202 mistaken for a result, a completed job whose assets you never fetched before the link expired. Assert on resp.model, on job.status, and on assets.length — never on the status code alone.

Where to go next

  • Quickstart — the five-minute version, chat only.
  • Unified API — full endpoint and scope reference.
  • Routing — strategies, auto, and fallback policy in depth.
  • Governance — budgets, per-key model policy, and audit.

On this page