diff --git a/automation/activepieces.mdx b/automation/activepieces.mdx
index 2d6b904..62ab054 100644
--- a/automation/activepieces.mdx
+++ b/automation/activepieces.mdx
@@ -95,7 +95,7 @@ Every job-shaped action and trigger returns the same flat row, so a table built
`cost_formatted` can be `null` on a row read the moment a job completes, because billing settles a moment later. A later read carries it.
-## Wait for a long job
+## How do I wait for a long job?
@@ -112,7 +112,7 @@ Turn **Wait for the Result** off to submit and move on, then pick up the result
- **Fail This Step if the Job Fails** is on by default, so a failed job runs the flow's error path. Turn it off to branch on `succeeded` instead.
- **Idempotency Key** is derived from the run, the step and the submission when left empty. An automatic retry then settles on the job it already created instead of billing a second one.
-## Start a flow when a job finishes
+## How do I start a flow when a job finishes?
**Finished Job** registers a webhook on your Rendobar account when the flow is turned on, and deletes it when the flow is turned off. **Outcome** picks which jobs start the flow:
diff --git a/automation/n8n.mdx b/automation/n8n.mdx
index 0fa00ac..afd5349 100644
--- a/automation/n8n.mdx
+++ b/automation/n8n.mdx
@@ -128,7 +128,7 @@ Every job type returns the same shape on completion, lifted to the top of the it
`file` is the headline result, or `null` for a job that computes data. `data` carries a probe report or a transcript, or `null` for a file-only job. `files` lists every output. **Raw** returns the full job and **Selected Fields** lets you pick.
-## Wait for a long job
+## How do I wait for a long job?
A transcode can take minutes and an hour-long render can take an hour. The node offers three ways to get the result, and only one of them is right for a long job.
@@ -155,7 +155,7 @@ The execution parks with no worker held. When the job ends, Rendobar's callback
Leave **Wait for Completion** off when you set a Callback URL. The node refuses the combination rather than picking one, because a hidden field in n8n keeps its value.
-## Start a workflow when a job ends
+## How do I start a workflow when a job ends?
**Rendobar Trigger** registers a webhook with Rendobar when the workflow activates and removes it on deactivation. Pick the events:
@@ -176,7 +176,7 @@ Pair `job.failed` with **Job > Get Logs** and the item carries the runner's log,
Rendobar must reach the webhook URL over HTTPS. That's true on n8n Cloud and on any publicly hosted instance. A `localhost` n8n is not reachable, so test locally through a tunnel such as `cloudflared tunnel --url http://localhost:5678` and set `WEBHOOK_URL` to its hostname.
-## Use it as an AI Agent tool
+## Can I use it as an AI Agent tool?
The node is marked usable as a tool, so it attaches to the AI Agent node's tool port and n8n builds the tool schema from the node's own parameters. Nothing else to define.
diff --git a/build-with-ai.mdx b/build-with-ai.mdx
index 6f7af8b..549c9c3 100644
--- a/build-with-ai.mdx
+++ b/build-with-ai.mdx
@@ -88,7 +88,7 @@ If I am talking to you inside an MCP-capable client (Claude, Cursor, and others)
Claude has no prefill link. In claude.ai or Claude Code, paste the prompt above, or connect the hosted [MCP server](/mcp-server) at `https://api.rendobar.com/mcp` over OAuth to run jobs straight from the conversation.
-## Keep it in your repo
+## How do I keep it in my repo?
Save the prompt as an agent rule so the context survives past the first session:
diff --git a/cli.mdx b/cli.mdx
index 667824b..5ed15ab 100644
--- a/cli.mdx
+++ b/cli.mdx
@@ -127,7 +127,7 @@ A delivery starts after the job completes. rb waits up to five minutes for it an
| `2` | Not authenticated, bad arguments, port in use, or insufficient credits |
| `130` | Cancelled with `Ctrl+C`. The CLI tries to cancel the in-flight job first |
-## Run it in CI
+## How do I run it in CI?
`RENDOBAR_API_KEY` is the only auth path for CI. It beats any saved credential and writes nothing to disk. Don't run `rb login` in a pipeline, the browser flow will hang.
@@ -162,7 +162,7 @@ rb ffmpeg --json -i in.mp4 -c:v libx264 out.mp4 | jq -r '.id'
The CLI has no `--idempotency-key`. If a retried step must not double-charge, submit via the [SDK](https://www.npmjs.com/package/@rendobar/sdk) or [`POST /jobs`](/jobs/ffmpeg) with an `idempotencyKey` field.
-## Common fixes
+## What do I do when a command fails?
`rb doctor` runs the install and connectivity checks and prints a `fix:` line under any failure. Run it first. The three you'll actually hit:
diff --git a/concepts/credits.mdx b/concepts/credits.mdx
index 18090b5..69c0502 100644
--- a/concepts/credits.mdx
+++ b/concepts/credits.mdx
@@ -51,25 +51,25 @@ For a full pricing breakdown and plan comparison, see [rendobar.com/pricing/](ht
$10 / $25 / $50 / $100 (or custom, $10 minimum). Pro adds **+20%** to every purchase. Buy $25, get $30. Credits never expire.
-## How it works: nanodollar billing
+## How does nanodollar billing work?
Every job is billed in nanodollars (one-billionth of a US dollar) drawn from your prepaid balance. This differs from tier-based quotas where you buy a plan with a bucket of minutes:
- **Cost matches resources used.** A 30-second 1080p job shouldn't cost the same as a 10-minute 4K render of a large master.
- **Prepaid means predictable budgets.** You top up when you want; jobs run only if affordable. No end-of-month surprise bill.
-## Credit lifecycle
+## When do credits expire?
1. **Submit gate.** Submit is atomic and does not reject on concurrency. If your balance is greater than zero the job is accepted. Jobs run at your plan's concurrency and any extra wait in a queue, starting in order as slots free. We do not estimate cost up front. A zero balance returns `402 INSUFFICIENT_CREDITS`, and a full queue returns `429 QUEUE_FULL`.
2. **Execution.** Job runs. Balance untouched.
3. **Debit on terminal status.** Every terminal job (`complete`, `failed`, or `cancelled`) is charged for the compute it consumed. The debit is atomic with the status update.
4. **No auto-refund.** Failed jobs are still charged for the compute they consumed. If you believe a job failed due to a platform bug, contact support. Refunds are issued at our discretion via the dispute process.
-## Negative balance
+## What happens when a balance goes negative?
Because the submit gate checks `balance > 0` rather than an estimate, a single long-running job can drive your balance briefly negative when the terminal debit lands. When that happens, further submits are blocked with `402 INSUFFICIENT_CREDITS` until you top up. Once your balance is positive again, jobs are accepted immediately. Nothing else on the account changes.
-## Cost model
+## How is a job priced?
`ffmpeg` bills per compute second, which is the wall-clock time FFmpeg ran your command, excluding upload and download. The same per-second rate applies whether the job completes successfully or fails partway through.
@@ -81,7 +81,7 @@ If your balance is at or below zero when you submit:
HTTP `402`. Top up at [app.rendobar.com/billing](https://app.rendobar.com/billing).
-## Check your balance
+## How do I check my balance?
```bash
curl https://api.rendobar.com/billing/state \
@@ -101,7 +101,7 @@ curl https://api.rendobar.com/billing/state \
`balance.amount` is in dollars. Internally balances are stored in nanodollars (1 USD = 1,000,000,000).
-## Usage tracking
+## How do I track usage?
```bash
curl "https://api.rendobar.com/billing/usage?start=2026-02-01&end=2026-02-09" \
diff --git a/concepts/generation.mdx b/concepts/generation.mdx
index 3d3ed1d..2f51ff8 100644
--- a/concepts/generation.mdx
+++ b/concepts/generation.mdx
@@ -42,7 +42,7 @@ Images are the first modality. You can generate an image from a text prompt, or
More modalities will join the same surface over time. Only image generation and editing are available today.
-## Tiers or exact models
+## Should I pick a tier or an exact model?
Every generation job takes a `model`. You can name a tier and let the platform choose, or pin an exact model id.
@@ -51,7 +51,7 @@ Every generation job takes a `model`. You can name a tier and let the platform c
Tier aliases can be re-pointed to newer models as the catalog grows. Pin an exact id when you need a result to stay stable across that change.
-## Discover models
+## How do I discover models?
`GET /models` returns the live catalog: every model with its supported jobs, tier, relative price, capabilities, and step range. Filter to one job type with `?job=`. Build a model picker against this endpoint instead of hardcoding the list.
@@ -83,7 +83,7 @@ curl "https://api.rendobar.com/models" \
A model with `underReview: true` is pulled from tier resolution while we re-evaluate its cost and quality. It stays listed so a caller pinning it can see why the submit was rejected, but no tier resolves to it and pinning it returns `VALIDATION_ERROR`.
-## Same job, same shape
+## Does every model use the same job shape?
A generation job is a [job](/concepts/job) like any other. You submit it to `POST /jobs`, it runs async, and you poll, [wait](/sdk#wait), or receive a [webhook](/guides/webhooks). The image jobs return a single webp file in `output.file`, with `output.data` set to `null`.
@@ -106,6 +106,7 @@ console.log(outputUrl(job)); // signed URL to the webp
- [Image generate](/jobs/image-generate): text to image, with the full model catalog
- [Image edit](/jobs/image-edit): instruction editing with one to four reference images
+- [Image upscale](/jobs/image-upscale): enlarge an image without a prompt
- [How a job works](/concepts/job): statuses and the output shape
- [SDK](/sdk): `jobs.run`, `jobs.wait`, and reading the output
- [Credits and billing](/concepts/credits): how compute-based billing works
diff --git a/concepts/job.mdx b/concepts/job.mdx
index 27ea061..561260c 100644
--- a/concepts/job.mdx
+++ b/concepts/job.mdx
@@ -44,7 +44,7 @@ sequenceDiagram
API-->>You: status complete, output
```
-## The shape of a job
+## What does a job look like?
Every job is a `type` plus the `inputs` and `params` that type expects:
@@ -62,7 +62,7 @@ To also write the output to a bucket you own, add `destinations`. An input can c
See [storage connections](/storage) for connecting a bucket and how outputs are named.
-## The lifecycle
+## What are the job statuses?
A job moves through six statuses. Three are terminal.
@@ -86,7 +86,7 @@ flowchart LR
On any terminal status the job is billed for the compute it used, and a [webhook](/guides/webhooks) fires if you configured one. A job that never started settles free. Failures carry an `error.code` from the [error catalogue](/support/errors). A job that runs past its plan's time limit fails with `JOB_TIMEOUT` and is billed for the compute it consumed. A job a runner accepted but never started fails with `RUNNER_TIMEOUT` and is not charged. Cancelling a `running` job stops the upstream execution too.
-## The output
+## What does a job return? {#the-output}
A `complete` job carries one `output`. The shape is the same for every type, so you never branch on the job type to find the result.
@@ -220,7 +220,7 @@ for (const f of job.output.files) console.log(f.path, f.type, f.size); // the fu
Three invariants hold for every type, so you can code against them: `output.file` is always one of `output.files` (or `null`), `expiresAt` is set whenever `files` is non-empty, and a `complete` job always has `output.data` or `output.files`. It never returns nothing.
-## Polling or webhooks
+## Should I poll or use webhooks?
Poll `GET /jobs/{id}` every second or two until the status is terminal, or skip polling entirely: register a [webhook](/guides/webhooks) and Rendobar pushes `job.completed` to your server the moment the job lands.
@@ -230,3 +230,7 @@ Poll `GET /jobs/{id}` every second or two until the status is terminal, or skip
- [Credits and billing](/concepts/credits): how the terminal debit works
- [Error codes](/support/errors): every `error.code` a failed job returns
- [FFmpeg](/jobs/ffmpeg): a job type end to end
+- [Compose](/jobs/compose): a render described as a JSON timeline
+- [Compress](/jobs/compress): a job that searches encodes to hit a budget
+- [Animated captions](/jobs/captions/animate): a job that transcribes and renders
+- [Generate an image](/jobs/image-generate): the same job shape on a diffusion model
diff --git a/concepts/scopes.mdx b/concepts/scopes.mdx
index 3c2ba61..0a9e95a 100644
--- a/concepts/scopes.mdx
+++ b/concepts/scopes.mdx
@@ -29,7 +29,7 @@ Every credential carries scopes, and every endpoint states the one it needs. Wha
comes from what it was granted, not from whether it authenticated with an API key, an OAuth token
or a dashboard session.
-## The scopes
+## Which scopes exist?
Seven resources, each with a read and a write scope.
@@ -49,7 +49,7 @@ Two rules cover the rest:
- **Self-description is never scoped.** Any credential can ask which workspace it acts for and what
limits apply, whatever else it holds.
-## Narrowing an API key
+## How do I narrow an API key?
Pass `scopes` when you create a key and it can do only that.
@@ -85,7 +85,7 @@ Two limits apply to every key, however it was made:
- **A key is never broader than what created it.** Asking for more than the creating credential
holds is refused rather than quietly narrowed.
-## What an app is asking for
+## What is an app asking for?
When you connect an app over OAuth, the consent screen lists what it requested, area by area, with
what each one lets it do. You can grant less than it asked for, and the app receives exactly what
@@ -99,7 +99,7 @@ it can reach and where. Revoking deletes the grant and every stored token, so th
access or renew what it has. An access token it already holds stays valid until it expires, which is
under an hour.
-## When a scope is missing
+## What happens when a scope is missing?
A request outside a credential's scopes is refused with `403` and a header naming what was missing,
per [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1).
diff --git a/docs.json b/docs.json
index e15ba56..dbe7218 100644
--- a/docs.json
+++ b/docs.json
@@ -304,6 +304,9 @@
"The full integration prompt for coding agents is at https://rendobar.com/prompts/integrate.md."
]
},
+ "metadata": {
+ "timestamp": true
+ },
"seo": {
"indexing": "all",
"metatags": {
diff --git a/guides/callbacks.mdx b/guides/callbacks.mdx
index 4c77a7a..af25fe2 100644
--- a/guides/callbacks.mdx
+++ b/guides/callbacks.mdx
@@ -27,7 +27,7 @@ canonical: "https://rendobar.com/docs/guides/callbacks"
A callback attaches a completion hook to one [job](/concepts/job). You pass `callback.url` when you create the job, and the moment that job reaches a terminal state Rendobar POSTs the result there. The body is byte-identical to the [webhook](/guides/webhooks) envelope, so the same receiver code handles both. The correlation lives in the URL you chose, so the receiver stays stateless and never polls `GET /jobs/{id}`.
-## Attach a callback
+## How do I attach a callback?
Add a `callback` object to `client.jobs.create(...)`. The only required field is `url`.
@@ -127,7 +127,7 @@ await client.jobs.create({
The waiting Workflow is the same as Shape A. It calls `waitForEvent({ type: "rb-job" })` and resumes when Cloudflare delivers the event.
-## Secure it
+## How do I secure a callback?
Callbacks are unsigned by default because the URL itself is the capability. Set `callback.verify: true` to sign the POST, then check it on your receiver. The signature is the same HMAC-SHA256 scheme as webhooks, over `${timestamp}.${body}`, and the SDK ships the same verifier.
@@ -148,7 +148,7 @@ const { data } = await client.orgs.getCallbackSecret();
Rotate the secret with `POST /orgs/current/callback-secret/rotate`. Reach for signing when the callback lands on an endpoint you own and you want to prove the POST came from Rendobar. For Shape B, where you already authenticate to Cloudflare through `callback.headers`, signing usually adds nothing.
-## Watch delivery
+## How do I watch delivery?
Once a job carries a callback, `GET /jobs/{id}` reports its delivery state under `callback.status`.
@@ -167,7 +167,7 @@ X-Rendobar-Timestamp: 1707436815
X-Rendobar-Attempt: 1
```
-## How it compares to org webhooks
+## How do callbacks compare to org webhooks?
[Webhooks](/guides/webhooks) and callbacks carry the identical envelope and verify with the same code. The difference is scope. A webhook is one static URL registered in the dashboard that receives events for every job in the organization. A callback is a per-job URL, set at create time, that receives events for exactly one job. Webhooks fit a central event pipeline. Callbacks fit an orchestrator that dispatches a job and waits on that one job, such as the Cloudflare Workflow above.
diff --git a/guides/webhooks.mdx b/guides/webhooks.mdx
index 69e9d79..6165bd9 100644
--- a/guides/webhooks.mdx
+++ b/guides/webhooks.mdx
@@ -31,7 +31,7 @@ Webhooks push events to your server the moment a [job](/concepts/job) changes st
Want a hook scoped to a single job instead of every job in the organization? See [per-job callbacks](/guides/callbacks).
-## Create an endpoint
+## How do I create an endpoint?
Create and manage endpoints on the [**Webhooks page**](https://app.rendobar.com/webhooks) in the dashboard.
@@ -60,7 +60,7 @@ Hit **Send Test** on the card to fire a sample event and confirm your receiver r
| `balance.low` | Credit balance crossed the low threshold |
| `balance.depleted` | Credit balance reached zero |
-## Verify every delivery
+## How do I verify a delivery?
Rendobar signs each payload. Check the signature before you trust the body. The SDK does it in one call.
@@ -132,7 +132,7 @@ def verify_webhook(body: bytes, signature: str, timestamp: str, secret: str) ->
Compare with a timing-safe function (`timingSafeEqual`, `hmac.compare_digest`). Plain string equality leaks the signature one byte at a time.
-## Payload
+## What is in the payload?
Every delivery carries these headers.
@@ -204,7 +204,7 @@ For `job.failed`, `data` carries `error` instead of `output`.
}
```
-## Delivery and retries
+## What happens when a delivery fails?
If your endpoint does not return `2xx` within 10 seconds, Rendobar retries up to 5 times, doubling the wait each attempt.
@@ -224,7 +224,7 @@ await client.webhooks.retryDelivery(deliveryId);
An endpoint that fails 10 deliveries in a row is disabled automatically and flagged in the dashboard. Update or re-enable it to reset the counter.
-## Secret rotation
+## How do I rotate the signing secret?
Rotate from the card, or with `client.webhooks.rotateSecret(endpointId)`. For 24 hours Rendobar signs with both secrets, sending `X-Rendobar-Signature` (new) and `X-Rendobar-Signature-Previous` (old), so `verifyWebhook` keeps passing while you roll the new one out. After the window the old secret stops signing. An endpoint can rotate once per 24 hours.
diff --git a/index.mdx b/index.mdx
index 9792a34..85003f5 100644
--- a/index.mdx
+++ b/index.mdx
@@ -4,6 +4,7 @@ sidebarTitle: "Overview"
description: "Start with the quickstart, then browse job types, storage connections, automation platforms, and the SDK, CLI and MCP server."
icon: "book-open"
keywords: ["rendobar docs", "rendobar documentation", "media processing api docs", "video api documentation", "ffmpeg api docs"]
+boost: 2
canonical: "https://rendobar.com/docs"
---
@@ -18,7 +19,7 @@ Rendobar runs media jobs on our infrastructure and hands back a finished file. Y
-## How a job works
+## How does a job work?
Every job shares the same shape, whatever its type. Read [how a job works](/concepts/job) for the lifecycle and the output contract, [scopes and access](/concepts/scopes) for what an API key is allowed to do, and [credits](/concepts/credits) for how work is billed.
@@ -65,6 +66,6 @@ Connect a bucket you own and jobs read inputs from it and write outputs back, so
-## When something goes wrong
+## What do I do when something goes wrong?
Check the [FAQ](/support/faq) first, then [error codes](/support/errors) to handle failures by machine code rather than message text. [Limits](/support/limits) covers plan caps, and [versioning](/support/versioning) covers how the API changes.
diff --git a/jobs/captions/animate.mdx b/jobs/captions/animate.mdx
index 4658551..b1b4efa 100644
--- a/jobs/captions/animate.mdx
+++ b/jobs/captions/animate.mdx
@@ -127,7 +127,7 @@ curl -X POST https://api.rendobar.com/jobs \
Also return the word-level transcript plus SRT and VTT on `output.data`.
-## Inputs
+## What inputs does it take?
URL of the source video. An uploaded asset's content URL works too.
@@ -141,7 +141,7 @@ curl -X POST https://api.rendobar.com/jobs \
URL of your own font file (TTF, OTF, TTC, WOFF, WOFF2). Captions render in it, and it takes priority over `style.font`, so you do not need to know the family name inside your own file. See [Fonts](#fonts).
-## Fonts
+## Which fonts can I use?
Eleven presets ship with sensible default fonts. To pick a different one, set `style.font` to a bundled family. No upload needed.
@@ -179,3 +179,11 @@ existing integration that does both keeps rendering the same way.
Like every job, this returns the standard response. See [Job output](/concepts/job#the-output) for the shape and how to read the result.
To burn existing subtitles instead, see [caption.burn](/jobs/captions/burn).
+
+## See also
+
+- [Burn subtitles](/jobs/captions/burn): static SRT or VTT burned into the frame
+- [Compose](/jobs/compose): build the video these captions sit on
+- [Compress](/jobs/compress): shrink the captioned output before you deliver it
+- [ffprobe](/jobs/ffprobe): check the audio stream before transcribing it
+- [Plan limits](/support/limits): duration and file-size caps per plan
diff --git a/jobs/captions/burn.mdx b/jobs/captions/burn.mdx
index 535f7ef..75e56b2 100644
--- a/jobs/captions/burn.mdx
+++ b/jobs/captions/burn.mdx
@@ -87,7 +87,7 @@ curl -X POST https://api.rendobar.com/jobs \
Omit `subtitles` to auto-transcribe. The spoken language is detected by default (`language: "auto"`). Pass an ISO-639-1 code to skip detection.
-## Inputs
+## What inputs does it take?
URL of the source video to caption.
@@ -178,3 +178,11 @@ Omit `subtitles` to auto-transcribe. The spoken language is detected by default
Like every job, this returns the standard response. See [Job output](/concepts/job#the-output) for the shape and how to read the result.
For animated word-level captions instead, see [captions.animate](/jobs/captions/animate).
+
+## See also
+
+- [Animated captions](/jobs/captions/animate): word-level captions generated from the audio
+- [ffprobe](/jobs/ffprobe): check the subtitle streams already in a file
+- [Compose](/jobs/compose): assemble the video before you burn subtitles onto it
+- [Error codes](/support/errors): the full error catalogue
+- [Plan limits](/support/limits): duration and file-size caps per plan
diff --git a/jobs/compose.mdx b/jobs/compose.mdx
index e9f93e1..cc761cc 100644
--- a/jobs/compose.mdx
+++ b/jobs/compose.mdx
@@ -616,7 +616,7 @@ choose, then reference that name in `style.font`.
```
There is no limit on how many you declare. TTF, OTF, TTC, WOFF and WOFF2 all
-work, and an [uploaded asset's](/api-reference/assets) content URL works in
+work, and an [uploaded asset's](/sdk#uploads) content URL works in
`src` like any other input URL.
Naming your own font is the point. The alternative is to reference the family
@@ -846,3 +846,12 @@ Fields on a text asset's `style`:
| `background` | Hex fill behind the text |
| `shadow` | `{ color, opacity, offsetX, offsetY }` |
| `animate` | `{ type, duration }` (type: fade, slideUp, slideDown, slideLeft, slideRight) |
+
+## See also
+
+- [Job output](/concepts/job#the-output): the output shape every job returns
+- [Animated captions](/jobs/captions/animate): word-level captions over a rendered video
+- [Compress](/jobs/compress): shrink the render to a size or quality budget
+- [FFmpeg](/jobs/ffmpeg): drop to a raw command when the timeline cannot express it
+- [Webhooks](/guides/webhooks): receive `job.completed` instead of polling
+- [Plan limits](/support/limits): render duration and file-size caps per plan
diff --git a/jobs/compress.mdx b/jobs/compress.mdx
index 282f52f..b5f4984 100644
--- a/jobs/compress.mdx
+++ b/jobs/compress.mdx
@@ -92,7 +92,7 @@ curl -X POST "https://api.rendobar.com/jobs" \
The compressed file lands in `output.file` and the report in `output.data`. Images finish in about 10 seconds, audio in about 1, video takes longer because every search probe is a real encode plus a VMAF pass.
-## The report
+## What does the report show?
Every completed job carries the search's account of itself:
@@ -116,7 +116,7 @@ Every completed job carries the search's account of itself:
`verdict` maps the score to a label. For images: 90+ is `visually_lossless`, 70+ is `high`, 50+ is `medium`, below that `low`. For video on VMAF: 95+, 90+, and 80+ (`acceptable`).
-## Targets
+## Should I target a size or a quality?
`target` takes one of four shapes:
@@ -129,7 +129,7 @@ Every completed job carries the search's account of itself:
The tier thresholds per medium: images clear SSIMULACRA2 90 / 85 / 78, video clears VMAF 96 / 93 / 88. Audio has no metric loop; tiers map to a channel-aware Opus bitrate ladder, so a mono voice track gets half the budget of stereo music.
-## Codecs
+## Which codecs does it use?
With `codec: "auto"`, the pick follows `compatibility`:
@@ -143,7 +143,7 @@ Or pin one: `av1`, `hevc`, `h264`, `vp9`, `avif`, `jxl`, `jpegli`, `webp`, `png`
`for: "archive"` skips the lossy ladder entirely and produces a lossless master: PNG for images, FFV1 in MKV for video, FLAC for audio.
-## Predict without encoding
+## Can I predict the result without encoding?
`dryRun: true` runs the search and returns the report with a predicted `outputBytes`, but writes no file and uploads nothing. `output.file` is `null`, the same shape as a data job. Use it to quote a compression before committing to it.
@@ -205,7 +205,7 @@ Or pin one: `av1`, `hevc`, `h264`, `vp9`, `avif`, `jxl`, `jpegli`, `webp`, `png`
Max execution time in seconds. Plan caps apply, same as [FFmpeg](/jobs/ffmpeg#parameters).
-## How the search behaves
+## How does the search pick an encode?
The search is a binary search over the encoder's quality scale, so a job costs about 7 probe encodes at most, each scored against the source. Videos longer than 12 seconds probe on three 2-second samples instead of the full file, and only the final encode runs over everything. A byte-budget search always encodes the full file, because the budget is exact.
@@ -222,4 +222,7 @@ After the job starts, failures carry the standard `error` shape. `INPUT_FETCH_FA
- [Job output](/concepts/job#the-output): the output shape every job returns
- [ffprobe](/jobs/ffprobe): inspect a file before deciding how to compress it
- [FFmpeg](/jobs/ffmpeg): full manual control when you want to write the command yourself
+- [Compose](/jobs/compose): render the video you are about to compress
+- [Upscale an image](/jobs/image-upscale): enlarge rather than shrink
+- [Plan limits](/support/limits): file-size caps and timeouts per plan
- [Webhooks](/guides/webhooks): receive `job.completed` instead of polling
diff --git a/jobs/ffmpeg.mdx b/jobs/ffmpeg.mdx
index 4aac700..36e6bda 100644
--- a/jobs/ffmpeg.mdx
+++ b/jobs/ffmpeg.mdx
@@ -85,7 +85,7 @@ curl -X POST https://api.rendobar.com/jobs \
-## How it runs
+## How does an FFmpeg job run?
1. **Validate**: command parsed against 8 security layers (flag whitelist, format/filter blocklists)
2. **Download**: input URLs pulled to a sandboxed runner
@@ -94,7 +94,7 @@ curl -X POST https://api.rendobar.com/jobs \
5. **Upload**: output goes to R2
6. **Return**: signed `output.file.url` available via `GET /jobs/{id}`
-## Input sources
+## What can I use as an input?
Most commands put the source URL straight in the command and skip `inputs` entirely. It is optional and defaults to an empty map:
@@ -200,7 +200,7 @@ This burns a subtitle track that lives only in the request, with no file to host
GPU jobs run the hardware encoders `h264_nvenc`, `hevc_nvenc`, and `av1_nvenc` on NVIDIA L4 GPUs, billed per second. `gpu` and `auto`-routed GPU jobs **require the Pro plan**. A GPU job on the Free plan returns `403 PLAN_LIMIT`. `cpu` and `auto` work on every plan. See [GPU acceleration](#gpu-acceleration).
-## Validate without executing
+## Can I validate a command without running it?
Free, no auth:
@@ -212,7 +212,7 @@ curl -X POST https://api.rendobar.com/ffmpeg/validate \
Returns `{ data: { valid: true, args: [...], inferredOutputFormat: "mp4" } }` or `{ data: { valid: false, error: "..." } }`.
-## Allowed flags
+## Which flags are allowed?
~120 whitelisted flags. Common ones:
@@ -234,7 +234,7 @@ Returns `{ data: { valid: true, args: [...], inferredOutputFormat: "mp4" } }` or
Unrecognised flags are rejected before dispatch with `VALIDATION_ERROR`.
-## GPU acceleration
+## Can I use GPU acceleration?
Your jobs run on CPU-powered or GPU-powered machines. `compute` defaults to `auto`, so a command that uses an NVENC encoder routes to a GPU on its own. You do not send anything.
@@ -268,7 +268,7 @@ Set `compute` only to force a choice. `gpu` pins the job to a GPU. `cpu` pins it
GPU jobs require the Pro plan and bill per second at GPU rates. A GPU job on the Free plan returns `403 PLAN_LIMIT`. CPU jobs run on every plan.
-## Security model
+## How is a command sandboxed?
Every command is validated before it runs, then executed in an isolated, network-restricted sandbox.
@@ -329,7 +329,7 @@ Plan job budgets: 1 hour (Free), 9 hours (Pro). That is a bound on processing ti
}
```
-## Output files
+## How do I return more than one file?
The filenames your command writes define the output. You get back every file it produced.
@@ -434,7 +434,7 @@ Consuming the result is the same every time. The answer is `output.data`. The th
URLs expire at `output.expiresAt`. Re-fetch the job with `GET /jobs/{id}` to mint fresh ones.
-## Error handling
+## How do I handle an error?
A failed job carries an `error` object with `code`, `message`, `detail`, and `retryable`. When FFmpeg exits non-zero, `error.detail` holds the last ~2 KB of the real stderr. Fetch the job to see exactly why FFmpeg stopped.
@@ -465,6 +465,10 @@ Full error catalogue: [Error codes](/support/errors).
- [Job output](/concepts/job#the-output): the output shape every job returns
- [CLI](/cli): run the same job from your terminal with `rb ffmpeg`
+- [Compose](/jobs/compose): describe a render as a JSON timeline instead of a command
+- [Compress](/jobs/compress): hit a size or quality budget without tuning flags yourself
+- [ffprobe](/jobs/ffprobe): inspect a file before you write the command
+- [Burn captions](/jobs/captions/burn): subtitles without building the filter graph
- [Webhooks](/guides/webhooks): receive `job.completed` instead of polling
- [Credits and billing](/concepts/credits): per-compute-second pricing detail
- [Plan limits](/support/limits): file-size caps, timeouts, and concurrency per plan
diff --git a/jobs/ffprobe.mdx b/jobs/ffprobe.mdx
index 5590cef..8f595bf 100644
--- a/jobs/ffprobe.mdx
+++ b/jobs/ffprobe.mdx
@@ -92,7 +92,7 @@ curl -X POST "https://api.rendobar.com/jobs" \
A probe is a data job. The answer lands in `output.data`, and `output.file` is `null` because nothing is written. Most probes finish in a few seconds. The SDK's `jobs.wait` polls for you; over raw HTTP, poll `GET /jobs/{id}` or subscribe with [webhooks](/guides/webhooks).
-## What comes back
+## What does ffprobe return?
Your flags are honored. Rendobar fills in only what you left out:
@@ -108,7 +108,7 @@ Four things are rejected with `VALIDATION_ERROR` before the job is created: `-pr
Rendobar reads the URL directly, so a faststart MP4 transfers only its header and a file over your plan's input limit still probes. When a server refuses range requests, the runner downloads the file first and the input limit applies.
-## The summary
+## What is in the summary?
`summary` normalizes what's awkward to read raw: rotation buried in side data, fractional frame rates, HDR spread across three color fields, cover art masquerading as video. Here is the sample file above, probed:
@@ -177,7 +177,7 @@ Every `summary` field name is stable. Write your checks against `summary.kind` a
Max execution time in seconds. Plan caps apply, same as [FFmpeg](/jobs/ffmpeg#parameters). A probe rarely approaches it.
-## Reading the result
+## How do I read the result?
On a JSON run, `output.data` carries `summary` plus every section ffprobe emitted, under ffprobe's own field names:
@@ -239,5 +239,8 @@ Read `error.retryable` rather than mapping codes yourself. A file that probes bu
- [Job output](/concepts/job#the-output): the output shape every job returns
- [FFmpeg](/jobs/ffmpeg): run a command against the file you probed
+- [Compress](/jobs/compress): shrink the file you just inspected
+- [Compose](/jobs/compose): use the probed dimensions to lay out a timeline
+- [Burn captions](/jobs/captions/burn): read the subtitle streams a probe reports
- [Webhooks](/guides/webhooks): receive `job.completed` instead of polling
- [Error codes](/support/errors): the full error catalogue
diff --git a/jobs/image-edit.mdx b/jobs/image-edit.mdx
index 10e23a5..f0b782e 100644
--- a/jobs/image-edit.mdx
+++ b/jobs/image-edit.mdx
@@ -91,7 +91,7 @@ print(job["output"]["file"]["url"]) # the edited webp
Each URL can be a public link or an [uploaded asset's](/sdk#uploads) content URL. The output size follows the first reference image unless you set `width` and `height`.
-## Reference images
+## How do reference images work?
`inputs.images` accepts one to four URLs. The per-model cap is tighter than the schema ceiling, so a request that clears the schema can still be rejected against the resolved model.
@@ -103,7 +103,7 @@ Each URL can be a public link or an [uploaded asset's](/sdk#uploads) content URL
Passing more images than the resolved model accepts returns `VALIDATION_ERROR` before anything is billed. Multiple references let you compose a scene, for example a product from one image on a background from another.
-## Tiers and models
+## Should I pick a tier or a model?
Set `model` to a tier alias or pin an exact model id. Omit it and you get the `economy` tier. Aliases can be re-pointed as the catalog grows, so pin an exact id when you need a result to stay stable.
@@ -176,7 +176,7 @@ requests.post(
-## The output
+## What does an edit job return?
A completed job carries a single webp file in `output.file`, typed `image`. `output.data` is `null`.
@@ -199,7 +199,7 @@ A completed job carries a single webp file in `output.file`, typed `image`. `out
The full [output shape](/concepts/job#the-output) is the same for every job type.
-## Watch it render
+## Can I watch it render?
An edit job emits `job.preview` events while the model denoises, each carrying a small webp frame. Subscribe with the SDK to show the edit resolving instead of a spinner.
@@ -254,7 +254,7 @@ Previews are decoration and never hold up a job. The `standard` tier finishes in
What to keep out of the edited image. Up to 1000 characters. Requires `qwen-image-edit-2511`.
-## Discover models
+## How do I discover models?
`GET /models?job=image.edit` lists the edit models with their reference-image caps, controls, and relative price. The [response shape](/jobs/image-generate#discover-models) is the same for both generation job types.
@@ -267,6 +267,7 @@ curl "https://api.rendobar.com/models?job=image.edit" \
- [Generation API](/concepts/generation): the modalities and the shared model catalog
- [Image generate](/jobs/image-generate): make an image from a text prompt
+- [Image upscale](/jobs/image-upscale): enlarge the edited result
- [Job output](/concepts/job#the-output): the output shape every job returns
- [SDK](/sdk): `jobs.run`, uploads, and reading the output
- [Webhooks](/guides/webhooks): receive `job.completed` instead of polling
diff --git a/jobs/image-generate.mdx b/jobs/image-generate.mdx
index e6a4ab8..e3adeb1 100644
--- a/jobs/image-generate.mdx
+++ b/jobs/image-generate.mdx
@@ -98,7 +98,7 @@ print(job["output"]["file"]["url"]) # the webp
No `inputs` are needed. The prompt is the whole request. Every generation job is async, so you submit, then poll, [wait](/sdk#wait), or receive a [webhook](/guides/webhooks).
-## The output
+## What does an image job return?
A completed job carries a single webp file in `output.file`. `output.data` is `null`, because generation writes a file rather than computing an answer.
@@ -121,7 +121,7 @@ A completed job carries a single webp file in `output.file`. `output.data` is `n
The real dimensions are in `output.file.meta`. Each model snaps your requested `width` and `height` to a size it can render, so the returned image can differ from what you asked for. The full [output shape](/concepts/job#the-output) is the same for every job type.
-## Watch it render
+## Can I watch it render? {#watch-it-render}
A generation job emits `job.preview` events while the model denoises, each carrying a small webp frame. Subscribe with the SDK to show the picture resolving instead of a spinner.
@@ -145,7 +145,7 @@ const sub = client.realtime.subscribeJob(created.id, {
Previews are decoration and never hold up a job. They are ephemeral: nothing is replayed on reconnect, and a late subscriber sees only the newest frame. A model with no fast decoder emits none, and a four-step model resolves too late to be worth watching. Build for zero frames and treat anything you get as a bonus.
-## Tiers and models
+## Should I pick a tier or a model?
Set `model` to a tier alias for a price and quality posture without naming a model, or pin an exact model id to reach that model's own controls. Omit `model` and you get the `economy` tier.
@@ -268,7 +268,7 @@ Tier aliases accept the base fields only. To use `steps`, `guidance`, or `negati
What to keep out of the image. Up to 1000 characters. Requires `qwen-image-2512`.
-## Discover models
+## How do I discover models? {#discover-models}
`GET /models` lists every generation model with its tier, relative price, capabilities, and step range, so a model picker never has to hardcode the catalog. Filter to one job type with `?job=`.
@@ -306,6 +306,7 @@ A bad request fails on `POST /jobs` before anything is billed. An unknown model
- [Generation API](/concepts/generation): the modalities and the shared model catalog
- [Image edit](/jobs/image-edit): change an existing image from an instruction
+- [Image upscale](/jobs/image-upscale): enlarge what you generated
- [Job output](/concepts/job#the-output): the output shape every job returns
- [SDK](/sdk): `jobs.run`, `jobs.wait`, and reading the output
- [Webhooks](/guides/webhooks): receive `job.completed` instead of polling
diff --git a/jobs/image-upscale.mdx b/jobs/image-upscale.mdx
index 48ffbcb..127c03e 100644
--- a/jobs/image-upscale.mdx
+++ b/jobs/image-upscale.mdx
@@ -92,7 +92,7 @@ print(job["output"]["file"]["url"]) # the png
-## Sizing
+## How do I control the output size?
`sizing` is a union, because "twice as big" and "2160 pixels tall" are different questions and only one can be answered per request.
@@ -137,11 +137,11 @@ two renders. A single large step is available with `passes: 1`, and looks worse.
Leave it at `1` for photographs. Lowering it hands the model less to work from and more to invent, which suits a clean uncompressed source and hurts a compressed one, where the reconstruction follows the compression artifacts instead of the subject.
-## Transparency
+## Is transparency preserved?
A transparent png stays transparent. Alpha is upscaled alongside the colour channels rather than flattened, so a logo keeps its edge.
-## The output
+## What does an upscale job return?
A completed job carries a single png file in `output.file`. `output.data` is `null`, because upscaling writes a file rather than computing an answer.
@@ -161,7 +161,7 @@ png rather than webp on purpose. Re-compressing lossily is the one thing that wo
}
```
-## Size limits
+## What are the size limits?
There is no product cap on how large an output you can ask for. The bound is what the GPU can physically hold.
@@ -183,3 +183,5 @@ The ceiling also depends on the model. `seedvr2-7b` holds about twice the weight
- [Generate an image from a prompt](/jobs/image-generate)
- [Edit an image from an instruction](/jobs/image-edit)
- [How the Generation API works](/concepts/generation)
+- [Compress the result](/jobs/compress): shrink the enlarged file before delivery
+- [Plan limits](/support/limits): resolution and file-size caps per plan
diff --git a/mcp-server.mdx b/mcp-server.mdx
index 383ccd3..a28c0e9 100644
--- a/mcp-server.mdx
+++ b/mcp-server.mdx
@@ -61,7 +61,7 @@ Clients without OAuth support can authenticate with an API key instead: header `
-## What you can do
+## What can I do with it?
Call `list_job_types` first. It returns every active job type fresh, so it never goes stale like a hardcoded list would. Pass `type` (for example `"compose"`) to get that one job type's complete parameter schema instead, with per-field documentation: ranges, defaults, units, and usage notes. Agents should do this before authoring a complex payload like a compose timeline.
@@ -83,7 +83,7 @@ Call `list_job_types` first. It returns every active job type fresh, so it never
A failed call returns `isError: true` with `{ error: { code, message, retryable } }` instead of throwing, so the agent can react to it. See the [error catalogue](/support/errors) for codes.
-## From your phone
+## Can I use it from my phone?
A phone has no filesystem the agent can reach, so a media file routes through a page you open yourself.
@@ -199,7 +199,7 @@ Cursor, Cline, Windsurf, Zed, and Continue all run the same `npx -y @rendobar/mc
Needs Node 20.10 or later. The server checks at startup and exits with a clear message if it's older.
-## Manage what it can reach
+## How do I limit what it can reach?
Approving a connection grants access to one workspace, and you can take it back.
diff --git a/quickstart.mdx b/quickstart.mdx
index c4db7d0..8eb3c86 100644
--- a/quickstart.mdx
+++ b/quickstart.mdx
@@ -4,6 +4,7 @@ sidebarTitle: "Quickstart"
description: "Submit your first ffmpeg job, poll to completion, and download the result. Under 5 minutes from API key to finished MP4."
icon: "rocket"
keywords: ["rendobar quickstart", "ffmpeg api tutorial", "first api call", "video processing quickstart", "ffmpeg job"]
+boost: 2
canonical: "https://rendobar.com/docs/quickstart"
---
@@ -35,7 +36,7 @@ Add Rendobar to my app: read and follow https://rendobar.com/prompts/integrate.m
```
-## 1. Get an API key
+## 1. Get an API key {#authentication}
Sign up at [app.rendobar.com](https://app.rendobar.com) and create a key from **Settings → API keys**. Keys start with `rb_`. New accounts get $5 in free [credits](/concepts/credits).
@@ -150,6 +151,10 @@ Every job type returns this same `output` shape: `data` for a computed answer, `
- [Rendobar CLI](/cli): skip the curl, run `rb ffmpeg` from your terminal
- [FFmpeg](/jobs/ffmpeg): full guide with the security model and examples
+- [Compose](/jobs/compose): render video from a JSON timeline
+- [Compress](/jobs/compress): hit a size or quality target
+- [Animated captions](/jobs/captions/animate): transcribe and burn word-level captions
+- [Generate an image](/jobs/image-generate): text to image on hosted models
- [Webhooks](/guides/webhooks): push events instead of polling
- [Credits and billing](/concepts/credits): plans, balance, billing
- [MCP](/mcp-server): drive Rendobar from an AI agent
diff --git a/storage/amazon-s3.mdx b/storage/amazon-s3.mdx
index 07ab159..eb48879 100644
--- a/storage/amazon-s3.mdx
+++ b/storage/amazon-s3.mdx
@@ -171,7 +171,7 @@ To change how outputs are named, see [delivery settings](/storage#delivery-setti
To create the role with Terraform or by hand, open **Trust policy and Terraform** in the waiting dialog and copy either one. Create the role with a maximum session duration of 12 hours, which the Terraform already sets. Then select **I created the role another way**, and paste the role's ARN. Rendobar runs the access checks as soon as you finish.
-## Revoke access
+## How do I revoke access?
Delete the stack in CloudFormation. This deletes the role, and Rendobar removes the connection. The OpenID Connect provider, and any bucket the stack created, stay in the account.
diff --git a/storage/cloudflare-r2.mdx b/storage/cloudflare-r2.mdx
index b9839d4..74a0ca9 100644
--- a/storage/cloudflare-r2.mdx
+++ b/storage/cloudflare-r2.mdx
@@ -131,7 +131,7 @@ With the default output path, this writes `exports/clip.mp4`. Delivery finishes
To change how outputs are named, see [delivery settings](/storage#delivery-settings). If a delivery fails, see [delivery errors](/storage#delivery-errors).
-## Revoke access
+## How do I revoke access?
1. In the Cloudflare dashboard, go to **R2 Object Storage** > [**Manage API tokens**](https://developers.cloudflare.com/r2/api/tokens/).
2. Delete the token used by the connection.
diff --git a/storage/index.mdx b/storage/index.mdx
index df0f25b..ab5a4f9 100644
--- a/storage/index.mdx
+++ b/storage/index.mdx
@@ -37,7 +37,7 @@ storage:///
-## Supported providers
+## Which providers are supported?
| Provider | Credentials |
|---|---|
@@ -48,7 +48,7 @@ storage:///
Use **S3-compatible** for any service that implements the S3 API, such as MinIO, Backblaze B2, or Wasabi.
-## How connections work
+## How does a storage connection work?
A connection has three parts:
@@ -275,7 +275,7 @@ A failed delivery includes one of these `reason` codes. Rendobar never forwards
Errors returned when you create a connection are listed in [error codes](/support/errors).
-## Credentials
+## How are credentials stored?
For connections that use keys, Rendobar stores the access key and secret, encrypted with AES-256-GCM. No API endpoint returns them. A one-click AWS role stores no keys, because Rendobar assumes the role when it needs access.
diff --git a/storage/s3-compatible.mdx b/storage/s3-compatible.mdx
index 1191a40..3156eac 100644
--- a/storage/s3-compatible.mdx
+++ b/storage/s3-compatible.mdx
@@ -116,7 +116,7 @@ If the access checks fail with a signature or bucket error and the keys are corr
**Public URLs.** Set your provider's public or CDN domain as the connection's [public URL](/storage#public-url), so deliveries return links on that domain.
-## Revoke access
+## How do I revoke access?
Delete or deactivate the access key in your provider's console. The connection stops working immediately.
diff --git a/storage/supabase.mdx b/storage/supabase.mdx
index f472a5e..55f7fab 100644
--- a/storage/supabase.mdx
+++ b/storage/supabase.mdx
@@ -141,7 +141,7 @@ Both credentials bypass Row Level Security, so your storage policies do not limi
The `service_role` key has full access to the project's data, not only Storage. If that is more access than you want to give, connect with S3 keys.
-## Revoke access
+## How do I revoke access?
**S3 keys.** In the Supabase dashboard, go to **Storage** > [**S3**](https://supabase.com/dashboard/project/_/storage/s3) and delete the access key. The connection stops working immediately.
diff --git a/support/errors.mdx b/support/errors.mdx
index 93e2795..34f1edf 100644
--- a/support/errors.mdx
+++ b/support/errors.mdx
@@ -77,7 +77,7 @@ Creating, testing, or updating a [storage connection](/storage) returns these co
A failed delivery does not return an error code. The job reports a `reason` for that destination instead. See [delivery errors](/storage#delivery-errors).
-## Handle the common ones
+## How do I handle the common errors?
The three you'll hit most: `INSUFFICIENT_CREDITS`, `RATE_LIMITED`, `VALIDATION_ERROR`.
@@ -160,6 +160,9 @@ if not res.ok:
## See also
+- [FFmpeg](/jobs/ffmpeg): the flag and command errors this job returns
+- [Compress](/jobs/compress): why a target can be unreachable
+- [Plan limits](/support/limits): the caps that produce a limit error
- [Limits](/support/limits)
- [Job lifecycle](/concepts/job)
diff --git a/support/limits.mdx b/support/limits.mdx
index 209b6d5..40cef77 100644
--- a/support/limits.mdx
+++ b/support/limits.mdx
@@ -45,7 +45,7 @@ Every limit Rendobar enforces, by plan. Pulled from `PLAN_LIMITS` in the API. Fo
`ffmpeg`, MCP, and webhooks are on both plans.
-## Output URLs
+## How long do output URLs last?
- **Signed download URL** (`output.file.url`) is valid for **1 hour**. Re-fetch via `GET /jobs/{id}` for a fresh URL.
- **Output retention**: 7 days (Free) or 30 days (Pro). After that, the file is deleted; the job record stays but the file URLs return 404.
@@ -64,7 +64,7 @@ $10 / $25 / $50 / $100 (or custom, $10 minimum). Pro adds +20% to every purchase
| Max page size | 100 |
| Max request body | 100 MB |
-## File formats
+## Which file formats are supported?
Rendobar accepts every FFmpeg-compatible input format. Default outputs by media type:
diff --git a/support/versioning.mdx b/support/versioning.mdx
index 40ad438..898027f 100644
--- a/support/versioning.mdx
+++ b/support/versioning.mdx
@@ -11,7 +11,7 @@ There is no version in the URL. No `/v1`, no `Api-Version` header, no date
pinning. That is a commitment, not an omission: the API only changes in ways
that existing callers survive, so there is nothing to pin to.
-## What we will change without warning
+## What changes without warning?
These are safe because a tolerant client ignores what it does not recognise.
@@ -24,7 +24,7 @@ These are safe because a tolerant client ignores what it does not recognise.
Write clients that ignore unknown response fields. Every SDK we publish already
does.
-## What we treat as breaking
+## What counts as a breaking change?
- Removing or renaming a field, parameter, endpoint or method
- Making a response field nullable, or changing its type
@@ -35,7 +35,7 @@ When one of these looks necessary we look for the additive path first: a new
sibling field, a default, a new endpoint alongside the old one. We break only
when there is genuinely no additive option.
-## How something goes away
+## How is something removed?
Three steps, in order, and the middle one lasts.
@@ -48,7 +48,7 @@ A deprecated surface is one you can keep using while you migrate, not one that
is about to fail. `client.team.*` and the `/team/*` routes are deprecated today
and call exactly the same code as their replacements.
-## How to tell what is deprecated
+## How do I tell what is deprecated?
- **OpenAPI**: the operation carries `"deprecated": true` in
[the spec](https://api.rendobar.com/openapi.json). Generators surface this.
@@ -57,7 +57,7 @@ and call exactly the same code as their replacements.
- **Changelog**: every deprecation is announced at
[rendobar.com/changelog](https://rendobar.com/changelog/).
-## SDK versions
+## How are SDK versions numbered?
[`@rendobar/sdk`](/sdk) follows semantic versioning.