---
title: Run invideo in CI and on servers
description: Sign in once with a device code, then run the invideo CLI from a script with JSON answers, capped spending, safe retries and a file on disk.
---
You can run invideo from a CI job, a cron script or a server with no browser. A person signs in once with a device code. After that, the script makes projects, uploads files, generates with a cap, exports, and saves the result to disk. Every answer is JSON, and every failure has an exit code your script can act on.

There are no API keys. Every command runs as a signed-in person, in their workspace.

## How it works

The CLI is one binary, and the same commands work in a terminal and in a script. Six things make it fit for a pipeline.

- **Device sign-in.** `INVIDEO_NO_BROWSER=1 invideo auth login` prints a link and a code, then waits until a person approves on their own device. The CLI keeps the sign-in on this machine. `invideo auth logout` forgets it.
- **A name in the project.** `invideo agent register` takes a name in a project, like `ci-render`. Set it once as `INVIDEO_AS`, or pass `--as` on every command. People watching the project see who did what.
- **JSON answers.** `--json` prints the whole answer as one JSON object, with `next` included. `INVIDEO_JSON=1` does that on every command. Errors and notices always go to stderr.
- **Waiting.** A command that starts a job returns the URL to follow at once. `invideo status <url> --wait` checks back until the job is over. `--timeout <s>` stops waiting after that many seconds. `invideo status` saves no file. `invideo notebook show <card-url> --out <dir>` saves what an export made, with the card's URL from the export answer's `.sequences[0]`.
- **Exit codes.** Each failure has its own code, so the script knows when to retry and when to stop and tell a person.
- **Safe retries and caps.** `--idempotency-key` on `invideo gen run` and `invideo edit export` makes a retry find the first job. `--max-credits` on `invideo gen run` stops a generation that costs more than the cap.

### Sign in on a machine with no browser

A person signs in once, on the machine that runs the jobs. Run this in the background, because it waits for the approval:

```bash
INVIDEO_NO_BROWSER=1 invideo auth login
```

It prints a link and a code at once. Send both to the person in one message. They open the link on their own device, enter the code, and choose the workspaces. Then check it:

```bash
invideo auth status
```

Never ask the person for a password, an email code or the code back.

A fresh container starts with no sign-in, and the CLI exits with code 4. For a runner that does not keep its state, use the MCP server over HTTP with the device sign-in and a refresh token kept in your secret store. The steps are on [Sign in, device codes and sessions](/account/sign-in).

### Exit codes in scripts

These come from `invideo help exit-codes`. Your script acts on each one.

| Code | Meaning | What the script does |
| --- | --- | --- |
| 0 | ok | Go on |
| 1 | failed (invideo's side, or unknown) | Stop and log the `message` |
| 2 | usage, or a request that cannot be done as asked | Fix the command |
| 3 | conflict: the timeline changed since your read; read it again | Run `invideo edit show` again and rebuild the change |
| 4 | not signed in: run `invideo auth login` | Stop. A person runs `invideo auth login` |
| 5 | a limit: credits or quota | Stop and tell a person |
| 6 | held by moderation: a safety review, not a refusal | Wait, then run `invideo status <url> --wait` again |
| 7 | transient: nothing ran | Run the same command again |
| 8 | needs the person's confirmation | Stop and tell a person |
| 9 | update required: run `invideo update`, then run it again | Run `invideo update`, then run it again |

With `--wait`, a command tries again by itself when nothing ran (exit 7). A retry is the same request, so it never starts anything twice.

### The confirmation hold

Some jobs stop with the status `needs_confirmation`, or exit 8. Only a person can confirm it, in invideo. A pipeline cannot confirm it. The script stops, logs the URL, and tells someone. When the person has confirmed it, run `invideo status <url> --wait` to pick the job up again.

## Steps

The script below runs one full job. It checks the sign-in, makes a project and an edit, uploads a file, generates a clip with a cap, places the upload on the timeline, checks the edit, exports, waits for the card, and saves the video. It needs `jq`.

Your agent runs a dry run first. For any call that costs 1 credit or more, it tells you the price and waits for your yes. A call under 1 credit runs without asking, and your agent tells you what it cost. In a pipeline, the person gives that yes once, as the cap.

<Steps>
<Step title="Sign in once">
A person runs the device sign-in above on the machine that runs the jobs. Expected result: `invideo auth status` shows their name.
</Step>
<Step title="Set the cap">
Price the generation once, by hand, before the first run.

1. Run `invideo gen models <project-url> --query "<job>"`, the CLI's `find_models`. A good pick is `seedance_2_5.r2v`. Your agent can search for others with `find_models`.
2. Run `invideo gen model <project-url> --model seedance_2_5.r2v` to read its inputs.
3. Run `invideo gen run` on the same URL as the script, with `--dry-run` and the real inputs, and tell the person the price.

`<project-url>` is any project of yours, from `invideo project new`. Set `MAX_CREDITS` to the price the person agreed to, from the dry run's `.estimated_credits`. Expected result: the script refuses to start without it.
</Step>
<Step title="Run the script">
Run the script below. Expected result: `./out/export.mp4` on disk and the export card's URL in the log.
</Step>
<Step title="Check the result">
Look at the frames in `./out/frames` before you ship the video. Expected result: the video looks as planned.
</Step>
</Steps>

```bash
#!/usr/bin/env bash
# One invideo render from a pipeline. Needs jq and a signed-in CLI.
set -euo pipefail
export INVIDEO_JSON=1
RUN="${RUN_ID:?set RUN_ID, unique to this run}"
MAX_CREDITS="${MAX_CREDITS:?set the agreed cap}"
OUT=./out

stop() {
  case "$rc" in
    4) echo "Not signed in: a person runs invideo auth login" ;;
    5) echo "Out of credits or quota: tell the account owner" ;;
    6) echo "Held by moderation: tell the account owner" ;;
    8) echo "Needs a person to confirm it in invideo" ;;
    9) echo "Run invideo update, then run the job again" ;;
    *) echo "invideo failed with exit $rc" ;;
  esac >&2
  exit "$rc"
}
iv() { invideo "$@" || { rc=$?; stop; }; }

# 1. Signed in?
iv auth status >/dev/null

# 2. A project, and a name in it
PROJECT=$(iv project new --title "Nightly $RUN" | jq -r .url)
iv agent register "$PROJECT" --name ci-render >/dev/null
export INVIDEO_AS=ci-render

# 3. An edit, and the source file in its media bin
EDIT=$(iv edit new "$PROJECT" --name "Nightly cut" | jq -r .url)
iv asset upload "$EDIT" ./media/intro.mp4 --wait >/dev/null

# 4. Generate: read the inputs, price it, then run it with a cap and a key
iv gen model "$PROJECT" --model seedance_2_5.r2v >/dev/null
iv gen run "$EDIT" --model seedance_2_5.r2v --dry-run \
  -i prompt=@prompt.txt
GEN=$(iv gen run "$EDIT" --model seedance_2_5.r2v \
  --max-credits "$MAX_CREDITS" --idempotency-key "$RUN-gen" \
  -i prompt=@prompt.txt | jq -r .url)
iv status "$GEN" --wait >/dev/null

# 5. Place the upload, then check and capture
SHOW=$(iv edit show "$EDIT")
TOKEN=$(jq -r .state_token <<<"$SHOW")
TL=$(jq -r .main_timeline_id <<<"$SHOW")
ASSET=$(iv edit show "$EDIT" --mode collection --collection asset \
  | jq -r '.objects[0].id')
jq -n --arg a "$ASSET" \
  '[{op:"insert_media",asset:$a,track:"V1",at_f:0,ripple:true}]' \
  > ops.json
iv edit change "$EDIT" --timeline "$TL" --token "$TOKEN" \
  --operations @ops.json
iv edit check "$EDIT"
iv edit capture "$EDIT" --at 0,1,2 --out "$OUT/frames"

# 6. Export, wait for the card, then save the file
CARD=$(iv edit export "$EDIT" --format video --quality 1080 \
  --idempotency-key "$RUN-export" | jq -r '.sequences[0]')
iv status "$CARD" --wait >/dev/null
iv notebook show "$CARD" --out "$OUT" >/dev/null
test -f "$OUT/export.mp4" || { echo "No file saved: $CARD" >&2; exit 1; }
echo "Exported: $CARD"
```

Notes on the script:

- `insert_media` places the picture alone. To place the file with its sound, use `place_media` with `linked_audio`. `invideo edit catalog <edit-url> --ops place_media` lists its fields.
- The generated clip lands in the edit's thread. To place it too, read its sequence URL with `invideo notebook show`, and pass that URL as `asset` in the operation.
- `invideo status` waits for the export and saves nothing. `invideo notebook show` on the card saves the file as `$OUT/export.mp4`, whatever `--file-name` says. The script checks the file is there before it reports success.
- The script reads the card's URL, the state token and the main timeline's id from the JSON answers. Run each command once with `--json` and check the keys before you rely on the script.
- A key only matches in its own project, for a generation, or on its own timeline, for an export. A rerun of the whole script makes a new project, so it pays again. To retry one step, run that command again with the same URL and the same key.

## If something goes wrong

Most pipeline failures are a lost sign-in, a stale token, or a hold that needs a person.

| Symptom | Fix |
| --- | --- |
| `get_model` or `generate` refuses the model as not found | Models retire. Run `find_models` with the job in words, use the best match, and tell the person which model you used. |
| Exit 4 on the first command | The machine has no sign-in. A person runs `INVIDEO_NO_BROWSER=1 invideo auth login` on it. |
| Exit 3 on `invideo edit change` | Someone edited first. Run `invideo edit show` again for a new token and rebuild the change. |
| Exit 5 | Credits or quota ran out. Stop and tell the account owner. |
| Exit code 6, or `status` is `held` | A safety review, not a refusal. Wait, then run `invideo status <url> --wait` again. Do not resend. If it ends `failed`, change the input the review flagged. |
| Exit 8, or status `needs_confirmation` | Only a person can confirm it, in invideo. Stop, log the URL, and tell someone. |
| Exit 7 | Nothing ran. Run the same command again, or add `--wait`. |
| Exit 9 | Run `invideo update`, then run the job again. |
| `--wait` stops before the job is over | Raise `--timeout`, or run `invideo status <url> --wait` again later. |
| The answer says you acted as a shared name | Register a name with `invideo agent register` and set `INVIDEO_AS`. |
| The script stops with "No file saved" | Read the card with `invideo notebook show`, then run the last line again. |
| The frames in `./out/frames` look wrong | Run the checks in [Check your video before you say it is done](/how-to/check-your-video). |
| Exit 0, but `./out` has no video | `invideo status` saves nothing. Run `invideo notebook show <card-url> --out ./out`, with the card's URL from `.sequences[0]` of the export answer. |
| Avoid: running `invideo gen run` again to check a job | Instead: `invideo status <url> --wait`. |
| Avoid: a new `--idempotency-key` on a retry | Instead: the same key, so the retry finds the first job. |
| Avoid: asking the person to paste a code or password | Instead: send the link and code from the device sign-in, and wait. |

## Related

- [Know the cost before you spend](/how-to/cost-before-you-spend)
- [Check your video before you say it is done](/how-to/check-your-video)
- [Sign-in](/account/sign-in)
- [CLI reference](/reference/cli)
- [Exit codes](/reference/exit-codes)
- [Environment variables](/reference/environment)