# Transcribe a link

> The server fetches a public URL or a YouTube video, then transcribes it like a file. Asynchronous, with a fetch phase first.

| Endpoint | What it does |
|---|---|
| `GET /v1/transcriptions/probe` | Probe a link |
| `POST /v1/transcriptions` | Create a transcription |
| `GET /v1/recordings/{id}/job` | Get a job |
| `GET /v1/recordings/{id}/transcript` | Get a transcript |

Give the API a link instead of a file and it fetches the media itself: a public `http(s)` address (an mp3, a video, a podcast episode) or a YouTube video. The rest is [Transcribe a file](https://api.vocalstack.com/docs/guides/transcribe-a-file.md): a job to wait on, then the transcript.

## 1. Check the link

`GET /v1/transcriptions/probe` says what a link is before anything is stored: `kind` is `file` (transcribe it here), `live` (a YouTube live, an endless radio stream, an HLS playlist with no end) or `unknown`, and `ok` is `false` with a `problem` sentence when the link did not open as media (a web page, an error). A live link never ends, so it is not a file: record it with a live transcription whose source is the link instead ([Go live](https://api.vocalstack.com/docs/guides/go-live.md)).

The probe is optional, and limited to 60 calls a minute. Links to private, loopback or cloud metadata addresses are refused.

## 2. Create the transcription

`POST /v1/transcriptions` with JSON `{ "url": "…" }` (and optional `name` and `settings`, as for a file) answers `201` at once; the server fetches the link afterwards. `vs.transcriptions.createFromUrl(url)` is the same call. An `Idempotency-Key` makes a retried create return the first transcription instead of a second one.

## 3. Wait for the fetch and the transcription

A link job starts in `fetching`, then moves to `transcribing` and `done` (or `failed`, with `error`). While it fetches, `progress.phase` is `fetching` (bytes arriving: `progress.fetchedBytes` of `progress.fetchTotalBytes`, when the other end says) or `extracting` (the audio is being taken out of a video), and `progress.phasePct` is how far that phase is. A long video takes a while to fetch; poll `GET /v1/recordings/{id}/job` every few seconds (`vs.jobs.wait` does) rather than in a tight loop.

## 4. Read the transcript

`GET /v1/recordings/{id}/transcript`, as for a file: `sentences`, `segments` with word timings, `speakers`, and `lang=` for a translation. The transcription keeps the link it came from (`job.source`).

## Complete code

Each program runs as it is once `VOCALSTACK_API_KEY` holds one of your API keys.

### TypeScript SDK: `link.ts`

```typescript
import { VocalStack } from '@vocalstack/sdk';

const vs = new VocalStack({ baseUrl: 'https://api.vocalstack.com', apiKey: process.env.VOCALSTACK_API_KEY });
const url = process.argv[2] ?? 'https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac';

const probe = await vs.transcriptions.probe(url);
if (!probe.ok) throw new Error(`that link did not open: ${probe.problem}`);
if (probe.kind === 'live') throw new Error('a live link: record it with a live transcription instead');

const tx = await vs.transcriptions.createFromUrl(url, { name: 'From a link' }, { idempotencyKey: crypto.randomUUID() });

const job = await vs.jobs.wait(tx.job!.recordingId, {
  intervalMs: 3000,
  onProgress: (j) => {
    const p = j.progress;
    if (j.state === 'fetching') console.log(`${p.phase ?? 'fetching'} ${p.phasePct ?? 0}%`);
    else console.log(`${j.state} ${p.pct}%`);
  },
});
if (job.state !== 'done') throw new Error(`transcription ${job.state}: ${job.error ?? ''}`);

const { sentences } = await vs.recordings.transcript(job.recordingId);
console.log(sentences.map((s) => s.text).join(' '));
```

### Python SDK: `link.py`

```python
import asyncio
import os
import sys
import uuid

from vocalstack import VocalStack

URL = sys.argv[1] if len(sys.argv) > 1 else "https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac"


def show(job) -> None:
    p = job.progress
    if job.state == "fetching":
        print(f"{p.phase or 'fetching'} {p.phase_pct or 0}%")
    else:
        print(f"{job.state} {p.pct}%")


async def main() -> None:
    async with VocalStack("https://api.vocalstack.com", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
        probe = await vs.transcriptions.probe(URL)
        if not probe.ok:
            raise SystemExit(f"that link did not open: {probe.problem}")
        if probe.kind == "live":
            raise SystemExit("a live link: record it with a live transcription instead")

        tx = await vs.transcriptions.create(url=URL, name="From a link", idempotency_key=str(uuid.uuid4()))
        assert tx.job is not None

        job = await vs.jobs.wait(tx.job.recording_id, interval_s=3, on_progress=show)
        if job.state != "done":
            raise SystemExit(f"transcription {job.state}: {job.error}")

        snapshot = await vs.recordings.transcript(job.recording_id)
        print(" ".join(s.text for s in snapshot.sentences))


asyncio.run(main())
```

### HTTP + WebSocket: `link.sh`

```bash
#!/usr/bin/env bash
# curl and jq, no SDK.
set -euo pipefail
API=https://api.vocalstack.com
AUTH="Authorization: Bearer $VOCALSTACK_API_KEY"
URL=${1:-https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac}

curl -sSfG "$API/v1/transcriptions/probe" -H "$AUTH" --data-urlencode "url=$URL" | jq '{kind, ok, problem}'

REC=$(jq -n --arg url "$URL" '{url: $url, name: "From a link"}' \
  | curl -sSf "$API/v1/transcriptions" -H "$AUTH" -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" -d @- \
  | jq -r .job.recordingId)

while :; do
  JOB=$(curl -sSf "$API/v1/recordings/$REC/job" -H "$AUTH")
  STATE=$(jq -r .state <<<"$JOB")
  jq -r '"\(.state) \(.progress.phase // "") \(.progress.phasePct // .progress.pct)%"' <<<"$JOB"
  case $STATE in
    done) break ;;
    failed) jq -r .error <<<"$JOB" >&2; exit 1 ;;
  esac
  sleep 3
done

curl -sSf "$API/v1/recordings/$REC/transcript" -H "$AUTH" | jq -r '[.sentences[].text] | join(" ")'
```


---

All guides: https://api.vocalstack.com/llms.txt. Every operation: https://api.vocalstack.com/llms-full.txt. The OpenAPI document, with every WebSocket frame under `x-websocket`: https://api.vocalstack.com/v1/openapi.json.
