# Create subtitles — Shorty API

> Starts an async subtitle job for media at the given URL. Responds 202 with a `Location` header and a `job_id` you can poll at GET /v1/jobs/{id}. Send an `Idempotency-Key` header to make retries safe. Exceeding the monthly free quota returns 429 (quota_exhausted); a Premium-only subtitle style or a free-plan duration/size cap returns 403 (feature_not_enabled); media over the hard duration/size ceiling returns 413 (request_too_large); an upstream start failure returns 503.

```http
POST /v1/subtitles
```

Starts an async subtitle job for media at the given URL. Responds 202 with a `Location` header and a `job_id` you can poll at GET /v1/jobs/{id}. Send an `Idempotency-Key` header to make retries safe. Exceeding the monthly free quota returns 429 (quota_exhausted); a Premium-only subtitle style or a free-plan duration/size cap returns 403 (feature_not_enabled); media over the hard duration/size ceiling returns 413 (request_too_large); an upstream start failure returns 503.

## Authentication

Requires a Bearer credential — a `shk_live_` API key or an OAuth 2.1 access token with the scope for this resource. See [Authentication](/docs/api).

## Parameters

| Name | In | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | header | No | Optional client-generated key (1–255 visible ASCII chars) that makes this POST replay-safe: a retry with the SAME key and body returns the original response (with `Idempotency-Replayed: true`) instead of creating a duplicate. Reusing a key with a different body is rejected (422). |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | `string<uri>` | Yes | A URL to the media to caption. |
| `language` | `string` | No | Language name ("english") or ISO-639-1 code ("en"). Defaults to english. (2–20 chars) Default: "english". |
| `style` | `"CLEAN" \| "TIKTOK" \| "PODCAST" \| "MINIMAL"` | No | Subtitle style. CLEAN is free; TIKTOK/PODCAST/MINIMAL require Premium. Default: "CLEAN". |
| `duration_seconds` | `number` | No | Measured media duration in seconds, if known. Lets the API reject media over your plan’s duration cap up front (413) instead of failing the job later. |

## Example request

curl:

```bash
curl -X POST https://aishorty.com/v1/subtitles \
  -H "Authorization: Bearer shk_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://example.com/clip.mp4"
  }'
```

TypeScript (@aishorty/sdk):

```typescript
import { Shorty } from '@aishorty/sdk'

const shorty = new Shorty({ apiKey: process.env.SHORTY_API_KEY })

const job = await shorty.subtitles.create({
    url: 'https://example.com/clip.mp4',
    style: 'TIKTOK',
})
```

## Response · 202

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `job_id` | `string` | Yes | The id of the accepted async job. |
| `status` | `string` | Yes | Initial job status (typically "queued"). |
| `tracking_url` | `string` | Yes | Relative path to poll this job’s status (GET /v1/jobs/{id}). |

## Errors

On failure this endpoint returns an RFC 9457 problem body. Possible statuses:

- `400`
- `401`
- `403`
- `409`
- `413`
- `422`
- `429`
- `500`
- `503`

Switch on the problem `code`, not the status. See the [error reference](/docs/api/errors) for every code and how to fix it.

> **Note:** This endpoint honors an `Idempotency-Key` header: a retry with the same key and body replays the original response for 24 hours instead of creating a duplicate job.
