# Conventions

> Base URL, field format, pagination, async jobs and expiring links: the rules that apply to every route.

The same rules apply to all routes. Read them once and the [reference](/reference) becomes predictable.

## Base URL

```text
https://api.cut.pro/api/v1
```

HTTPS only. The version is part of the path.

## Request

Send the body as JSON with `Content-Type: application/json`. The routes also accept `application/x-www-form-urlencoded` and `multipart/form-data` with the same fields, which helps in no-code tools.

## Fields

| Rule | Example |
|---|---|
| Every field is `snake_case` | `credits_cost`, `output_resolution` |
| The resource's own id is always `id` | `{ "id": "7412908371004912" }` |
| A reference to another resource carries its name | `video_id`, `edit_id`, `connection_id` |
| IDs are strings | store them as strings, never convert to a number |
| Date and time in ISO 8601, UTC | `"2026-10-29T18:04:51.000Z"` |
| Duration and position in the video in seconds | `"start_time": 42.5` |
| A field with no value comes as `null`, it does not disappear | `"completed_at": null` |

The only exception is `metadata` in `POST /posts`: the keys there are each network's own (`privacyLevel`, `categoryId`, `privacyStatus`) and go to the platform exactly as you wrote them.

## State and error

`status` describes where a job is and comes in lowercase: `queued`, `processing`, `completed`, `failed`.

`code` describes what went wrong and comes in uppercase: `INSUFFICIENT_CREDITS`, `SUBMISSION_EXPIRED`. Every error has the same body, with no text message:

```json
{ "code": "INSUFFICIENT_CREDITS", "extra": { "credits_needed": 121, "current_balance": 40 } }
```

Use a `switch` on `code` and translate it for your interface. See [Errors](/en/errors).

## Lists and pagination

Every list responds with the resource name and a `pagination` block:

```json
{
  "clips": [{ "id": "7412908412887301" }],
  "pagination": {
    "current_page": 1,
    "total_pages": 3,
    "total_count": 52,
    "has_next_page": true,
    "has_previous_page": false,
    "limit": 20
  }
}
```

Paginate with `page` (starts at 1) and `limit` (maximum 100). The default `limit` changes per route and is in each route's reference.

```bash
curl "https://api.cut.pro/api/v1/renders?page=2&limit=100" \
  -H "X-Api-Key: $CUTPRO_API_KEY"
```

Keep going while `has_next_page` is `true`.

## Async jobs

Clipping, render, post, transcription, generation and persona do not finish in the response. Creation returns an `id` and you poll until the end:

1. Create the job and keep the `id`.
2. Poll the job's read route every 5 to 10 seconds.
3. Stop when the `status` reaches a final state.

| Job | Read route | Final states |
|---|---|---|
| Clipping | `GET /clips/{videoId}/submissions/{submissionId}` | `completed`, `failed` |
| Render | `GET /renders/{renderId}` | `completed`, `failed`, `cancelled`, `expired` |
| Post | `GET /posts/{id}` | `completed`, `partial`, `failed`, `cancelled` |
| Transcription | `GET /transcriptions/{transcriptionId}` | `ready`, `no_speech`, `failed` |
| Generation | `GET /generations/{id}` | `ready`, `failed` |

This pace fits comfortably within the [rate limit](/en/rate-limits). The API does not send webhooks: the end of a job is always found by polling.

## Expiring links

| What | Validity |
|---|---|
| `download_url`, `play_url` and `thumbnail_url` of a clip | short: ask again when you are going to use it |
| `GET /renders/{renderId}/download` link | 1 hour |
| A render's MP4 file | set by the plan, in `render_expiry_hours` of `GET /renders/limits` |
| Edit (`edit_id`) | does not expire |

Do not store a signed link in your database. Store the `id` and ask for the link on the spot. If the MP4 was already deleted, `POST /renders` with the `edit_id` generates a new one.

## OpenAPI specification

The complete specification is at [`https://api.cut.pro/api/v1/openapi.json`](https://api.cut.pro/api/v1/openapi.json). Use it to generate a typed client, import into Postman or Insomnia, or give context to a coding agent.
