Your first Versely generation from the terminal
Install the CLI, mint a key, dispatch an image and a video job over HTTP, poll status, and pull the finished file down without opening the web app.
The first thing to be clear about, because it saves you ten minutes of running versely --help looking for something that is not there: the Versely CLI does not have a generate subcommand. Its job is authentication and wiring. It mints a key, stores it safely, and connects Versely's MCP server to the agents on your machine.
The generation itself is plain HTTP. That turns out to be the better arrangement, because it means the thing you learn in the next fifteen minutes is the same thing your cron job, your CI step and your agent are all doing underneath. One request shape, everywhere.
Install and sign in
Two commands. The package installs a versely binary.
npm install -g @versely/cli
versely auth login
versely auth login opens your browser and runs an authorization-code flow with PKCE over a localhost loopback redirect, the pattern described in RFC 8252 for native apps. No password is typed into the terminal and no client secret is stored. You approve access in the browser once, and that sign-in is used to mint a long-lived API key with a vsk_ prefix.
The key lands at ~/.versely/config.json, written so only your user can read it. There are no short-lived tokens to refresh afterwards, which is the property that makes this usable from a script six months later.
If you are on a headless box, in CI, or simply do not want a browser involved, skip the flow entirely:
versely auth login --key vsk_xxxxxxxx
You can also create a key directly in your account settings and paste it in that way. Confirm what you ended up with:
versely auth whoami
# API key: vsk_ab…9f2c (/Users/you/.versely/config.json)
versely status prints the same auth line plus per-agent MCP connection state, which is the faster command when something has stopped working and you do not yet know which half is broken. Full command reference on the CLI page.
Get the key into your shell
Every request needs a bearer token. The CLI reads VERSELY_API_KEY from the environment in preference to anything on disk, so exporting it is both the documented override and the convenient one:
export VERSELY_API_KEY=$(jq -r .apiKey ~/.versely/config.json)
export VERSELY_API_URL="https://api.versely.studio"
Check the balance before you spend anything. There is no free allowance on any surface, so an empty balance is a 402 rather than a queued job:
curl -s "$VERSELY_API_URL/api/v1/user/me" \
-H "Authorization: Bearer $VERSELY_API_KEY" | jq '.credits'
One quirk worth internalising now: never send a user_id in a request body. The key resolves the account server-side, and passing an id is at best ignored.
Your first image job
Generation is asynchronous everywhere. You submit, you get a request id, you poll. The submit looks like this:
curl -s -X POST "$VERSELY_API_URL/api/v1/generate/image" \
-H "Authorization: Bearer $VERSELY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Imagen 4",
"prompt": "a ceramic pour-over dripper on a walnut counter, morning window light from camera left, shallow depth of field",
"aspect_ratio": "16:9"
}'
model and prompt are the required pair. aspect_ratio takes the usual set — 1:1, 16:9, 9:16, 4:3, 3:4 — but each model publishes the ratios it actually accepts on its catalog page, and they are not all the same. Model names are the display names from the catalog, spelled exactly as they appear there, so Imagen 4 is "Imagen 4" and not a slug.
The response is a batch envelope even when you sent one item, which surprises people the first time:
{
"success": true,
"data": {
"successful": [
{ "data": { "requestId": "abc123", "status": "IN_QUEUE", "model": "Imagen 4" } }
],
"failed": [],
"total": 1,
"successCount": 1,
"failCount": 0
}
}
That shape exists because model also accepts an array. Passing ["Imagen 4", "Nano Banana Pro"] fans the same prompt across both and returns a requestId per model, which is the cheapest way to run a side-by-side without writing a loop. Pull the id you need:
REQUEST_ID=$(echo "$RESPONSE" | jq -r '.data.successful[0].data.requestId')
Poll for the result
One endpoint, whatever you generated:
curl -s "$VERSELY_API_URL/api/v1/status/$REQUEST_ID" \
-H "Authorization: Bearer $VERSELY_API_KEY"
It returns status as generating, completed or failed, along with type, model, created_at, and on completion a result_url plus a result_urls array for multi-output jobs. A five-second interval per request id is a sensible cadence — often enough to feel responsive, slack enough that a long video job is not costing you hundreds of wasted calls. Images typically land in seconds; video is a minutes-long job depending on model and length.
Wrapped up, with a ceiling so a stuck job does not spin forever:
for i in $(seq 1 60); do
STATUS=$(curl -s "$VERSELY_API_URL/api/v1/status/$REQUEST_ID" \
-H "Authorization: Bearer $VERSELY_API_KEY")
STATE=$(echo "$STATUS" | jq -r '.status')
case "$STATE" in
completed) echo "$STATUS" | jq -r '.result_url'; break ;;
failed) echo "job failed" >&2; break ;;
esac
sleep 5
done
Then pull the file down with curl -o or wget against result_url like any other asset. Poll the status endpoint rather than looking for the asset anywhere else; it is scoped to one request and it is the only view that will not race you.
The same thing for video
Same pattern, different path and a few more knobs:
curl -s -X POST "$VERSELY_API_URL/api/v1/generate/video" \
-H "Authorization: Bearer $VERSELY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Sora 2 Text to Video",
"prompt": "slow dolly-in on a rain-streaked window, city lights bokeh beyond, night",
"duration": "8",
"aspect_ratio": "9:16",
"resolution": "720p"
}'
duration is in seconds and resolution is an output height, and both are the parameters people get wrong first, because the accepted values are per model rather than global. Sora 2 Text to Video tops out at 720p and takes a fixed set of durations; VEO 3.1 goes to 4K and takes a different set. Read the model's catalog page before you hard-code either. Image-to-video models take image_url; first-and-last-frame models take first_frame_url and last_frame_url; Kling models additionally accept negative_prompt, cfg_scale and generate_audio. Every model lists its supported parameters on its own page, Sora 2 Text to Video included.
One detail that matters the moment output leaves the terminal and lands in an editor: Versely's default video frame rate is 25 fps, not 24. If you are cutting generated clips against 24 fps source material, that mismatch is worth handling at the timeline rather than discovering at export.
What breaks first
In rough order of how often you will hit them:
| Status | What it actually means | Fix |
|---|---|---|
| 401 | Key invalid, expired or revoked | versely auth whoami, then re-login |
| 402 | Insufficient credits | Top up; every call consumes credits |
| 403 | Key lacks the required scope | Mint a key with the scope you need |
| 429 | Rate limited | Read X-RateLimit-Reset, back off, retry once |
| 500 | Server-side | Retry once after ten seconds |
Rate limits are attached to the key rather than to the endpoint you called, and generation endpoints are held to a tighter per-minute ceiling than everything else. Rather than hard-code a figure that can change under you, read what the response tells you: every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a well-behaved script backs off against X-RateLimit-Reset instead of guessing.
The other thing to know before you scale up: the same job consumes half as many credits when you run it with an API key instead of in the app. That is a consumption difference rather than a different credit value, and it is the reason a repetitive, already-specified workload is worth moving out of the browser once it stops changing. The detail is on the API pricing page.
FAQ
Do I need the CLI at all if I have an API key?
No. A key from your account settings and curl is a complete setup, and VERSELY_API_KEY in the environment takes precedence over anything the CLI stored. The CLI earns its place when you also want the MCP server wired into Claude Code or Cursor, because versely install writes that config for you instead of you hand-editing JSON. The developer page covers the key-only path.
Is there a free tier for API calls?
No. Every generation consumes credits from the same balance as the app, on every surface, and the cheapest model in the catalog still costs credits. There is no free allowance and no separate API wallet, so you need an active subscription or a credit pack before the first request will run. How credits work covers the unit.
How do I know what a job will cost before I send it?
The catalog publishes a credit cost per model, and the agent surface exposes an estimate call that prices a planned list of items and returns the total against your current balance using the same logic as the real charge. From a bare shell the practical pre-flight is the balance check against your own arithmetic; for anything expensive, estimating in the agent first is faster than reconstructing the pricing yourself.
Can the CLI itself run a generation for me?
Not directly. versely covers setup, auth, install, uninstall and status. If you want a natural-language path from the terminal rather than raw HTTP, the move is versely install to connect MCP to your agent and then ask the agent, or install the skill packs so it knows the procedures. The skills page and the MCP page cover those two doors.