> ## Documentation Index
> Fetch the complete documentation index at: https://docs.framelane.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Upload media, submit your first render, and download the output in under 10 minutes.

## 1. Get your API key

**Humans:** sign up at [console.framelane.io](https://console.framelane.io) and copy your API key from **Settings → API Keys**.

**Agents (no human, no browser):** provision a key over the API. `POST /v1/signup` returns a key immediately and emails a 6-digit code; read it from the inbox you signed up with, then confirm it with `POST /v1/signup/verify` to activate the key. Until you verify, the key returns `403 email_not_verified` on every call. See [Self-provisioning for agents](/introduction/self-provisioning).

<CodeGroup>
  ```bash Sign up theme={null}
  curl -X POST https://api.framelane.io/v1/signup \
    -H "Content-Type: application/json" \
    -d '{"workspace_name": "my-agent", "email": "agent@example.com"}'
  # → 201 { "api_key": { "key": "fl_..." }, "otp_delivered": true }
  ```

  ```bash Verify the OTP theme={null}
  curl -X POST https://api.framelane.io/v1/signup/verify \
    -H "Content-Type: application/json" \
    -d '{"email": "agent@example.com", "otp_code": "123456"}'
  # the fl_ key from signup works the moment this returns 200
  ```
</CodeGroup>

## 2. Upload your input file

If your video is already hosted at a public HTTPS URL you control, skip to [step 3](#3-submit-a-render) and use that URL as `source_url`.

To pull in a file from a **remote URL** you don't control (another CDN, a cloud share link), set **`ingest_external: true`** on the render and pass the URL as the element `source_url` — Framelane copies it into your workspace first.

For a **local file**, request an upload slot and PUT the bytes to GCS:

<CodeGroup>
  ```bash Request upload URL theme={null}
  curl -X POST https://api.framelane.io/v1/uploads \
    -H "Authorization: Bearer $FRAMELANE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"content_type": "video/mp4", "filename": "input.mp4"}'
  ```

  ```bash Upload file bytes theme={null}
  # Save upload_url and source_url from the response above
  curl -X PUT "$UPLOAD_URL" \
    -H "Content-Type: video/mp4" \
    --data-binary @input.mp4
  ```
</CodeGroup>

You will use `source_url` from the upload response in the render request. See [Uploading media](/introduction/uploads) for supported MIME types and error codes.

## 3. Submit a render

Send a `POST /v1/renders` request with your composition. Replace `source_url` with your upload URL or any public HTTPS URL:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.framelane.io/v1/renders \
    -H "Authorization: Bearer $FRAMELANE_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: quickstart-render-1" \
    -d '{
      "width": 1920,
      "height": 1080,
      "duration": 5.0,
      "frame_rate": 30,
      "output_format": "mp4",
      "elements": [
        {
          "type": "video",
          "id": "clip1",
          "source_url": "https://cdn-user.framelane.io/uploads/ws_01J.../input.mp4",
          "time": 0,
          "in_point": 0,
          "out_point": 5
        }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  import Framelane from "@framelane/sdk";

  const client = new Framelane({ apiKey: process.env.FRAMELANE_API_KEY });

  const render = await client.renders.create({
    width: 1920,
    height: 1080,
    duration: 5.0,
    frameRate: 30,
    outputFormat: "mp4",
    elements: [
      {
        type: "video",
        id: "clip1",
        sourceUrl: process.env.SOURCE_URL!,
        time: 0,
        inPoint: 0,
        outPoint: 5,
      },
    ],
  });

  console.log(render.id, render.status); // render_01J... queued
  ```

  ```python Python theme={null}
  import os
  import framelane

  client = framelane.Framelane(api_key=os.environ["FRAMELANE_API_KEY"])

  render = client.renders.create(
      width=1920,
      height=1080,
      duration=5.0,
      frame_rate=30,
      output_format="mp4",
      elements=[
          {
              "type": "video",
              "id": "clip1",
              "source_url": os.environ["SOURCE_URL"],
              "time": 0,
              "in_point": 0,
              "out_point": 5,
          }
      ],
  )

  print(render.id, render.status)  # render_01J... queued
  ```
</CodeGroup>

The API returns **202 Accepted** immediately with a job object:

```json theme={null}
{
  "id": "render_01J8QR2K5VKDGN2T4FBM3CZYX7",
  "kind": "render",
  "status": "queued",
  "progress_percent": 0,
  "created_at": "2026-05-21T07:00:00Z",
  "updated_at": "2026-05-21T07:00:00Z"
}
```

Save the `id` — you need it to poll status or download the result.

## 4. Wait for completion

### Option A — Poll job status (simplest for scripts)

Call `GET /v1/renders/{id}` until `status` is terminal (`completed`, `failed`, or `cancelled`):

```bash theme={null}
curl https://api.framelane.io/v1/renders/render_01J8QR2K5VKDGN2T4FBM3CZYX7 \
  -H "Authorization: Bearer $FRAMELANE_API_KEY"
```

While processing, `status` is `queued` or `processing` and `progress_percent` updates over time. When `status` is `completed`, the response includes `output.url`:

```json theme={null}
{
  "id": "render_01J8QR2K5VKDGN2T4FBM3CZYX7",
  "status": "completed",
  "progress_percent": 100,
  "output": {
    "url": "https://cdn-user.framelane.io/render_01J.../output.mp4",
    "width": null,
    "height": null,
    "duration": null,
    "size_bytes": null
  }
}
```

<Note>
  `output.url` is the field to use — download the finished video from it. The `width`, `height`, `duration`, and `size_bytes` fields are reserved and currently return `null`; the output dimensions and duration match your render request, and the byte size is available from the download response's `Content-Length`.
</Note>

Poll every few seconds in a loop, or use the SDK helper:

```typescript theme={null}
const completed = await client.renders.waitForCompletion(render.id);
```

```python theme={null}
completed = client.renders.wait_for_completion(render.id)
```

The same polling pattern applies to tasks via `GET /v1/tasks/{id}`.

### Option B — Webhooks (recommended for production)

Register a webhook endpoint and Framelane calls it when the render completes:

```bash theme={null}
curl -X POST https://api.framelane.io/v1/webhooks \
  -H "Authorization: Bearer $FRAMELANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/framelane",
    "events": ["render.completed", "render.failed"]
  }'
```

When complete, you'll receive:

```json theme={null}
{
  "type": "render.completed",
  "data": {
    "id": "render_01J8QR2K5VKDGN2T4FBM3CZYX7",
    "status": "completed",
    "output": {
      "url": "https://cdn-user.framelane.io/render_01J.../output.mp4",
      "width": 1920,
      "height": 1080,
      "duration": 5.0,
      "size_bytes": 8388608
    }
  }
}
```

## 5. Download the artifact

The `output.url` in the job response or webhook is a signed CDN URL. You can also request a fresh download redirect:

```bash theme={null}
curl -L https://api.framelane.io/v1/renders/render_01J.../download \
  -H "Authorization: Bearer $FRAMELANE_API_KEY" \
  -o output.mp4
```

## Next steps

<CardGroup cols={2}>
  <Card title="Uploading media" icon="cloud-arrow-up" href="/introduction/uploads">
    Local files via signed GCS upload, or remote URLs with `ingest_external`.
  </Card>

  <Card title="Run an AI task" icon="bolt" href="/tasks/transcribe">
    Transcribe audio, remove backgrounds, or upscale video.
  </Card>

  <Card title="Verify webhook signatures" icon="shield-check" href="/webhooks/signature-verification">
    Validate HMAC signatures to ensure payloads come from Framelane.
  </Card>

  <Card title="API Reference" icon="book" href="/api-reference/introduction">
    Browse all endpoints generated from the OpenAPI spec.
  </Card>
</CardGroup>
