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

# Sources

# Sources

A **source** is a transcribed file uploaded into a project — typically a
voice memo, podcast snippet, or article import. Sources feed the AI
pipeline that produces notes and post drafts; they're not directly
visible to end users in the published feed.

The v1 API ships a **single-resource read** endpoint for sources. There
is no list endpoint in v1; if you need to enumerate sources, traverse
notes (`GET /v1/notes`) and follow each note's source attachment when it
ships in v1.x.

***

## `GET /v1/sources/{sourceId}`

Read a single source row. The transcription body is **truncated to the
first 2000 characters** for safety — long-form transcripts are excluded
from the API surface, intentionally:

* Sources can hold customer audio they uploaded with no expectation it
  would leave the dashboard. Exposing the full body would be a surprise.
* Some sources are large (multi-megabyte transcripts of hour-long calls).
  Pulling them through a JSON response would amplify rate-limit usage
  without any real client benefit.

If you need the full transcript for a programmatic workflow, file a
feature request and we'll consider a streaming download endpoint in
Phase 4.

```bash theme={null}
curl -i https://api.scripe.io/v1/sources/src_01J9ZA… \
  -H "Authorization: Bearer scripe_sk_live_…" \
  -H "Scripe-Api-Version: 2026-08-01"
```

### Response

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "id": "src_01J9ZA…",
    "projectId": "proj_01J9ZA…",
    "status": "Done",
    "name": "founder-podcast-2026-05-15.m4a",
    "fileType": "audio/mp4",
    "durationSeconds": 482,
    "createdAt": "2026-05-15T08:21:00.000Z",
    "updatedAt": "2026-05-15T08:23:11.000Z",
    "textPreview": "Hi everyone, today I want to talk about how we …",
    "textPreviewTruncated": true,
    "hasFullText": true
  }
}
```

### Field reference

| Path                   | Type              | Notes                                                                                                                                    |
| ---------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                   | string (`src_*`)  |                                                                                                                                          |
| `projectId`            | string (`proj_*`) | The owning project.                                                                                                                      |
| `status`               | string            | `Processing` \| `Done` \| `Failed`. Treat unknown values as `Processing`.                                                                |
| `name`                 | string \| null    | Original filename if known.                                                                                                              |
| `fileType`             | string \| null    | MIME type of the upload (best-effort; sometimes inferred).                                                                               |
| `durationSeconds`      | integer           | `0` for non-audio sources (text imports, etc.).                                                                                          |
| `createdAt`            | string (ISO 8601) |                                                                                                                                          |
| `updatedAt`            | string \| null    | `null` if never updated since creation.                                                                                                  |
| `textPreview`          | string            | First **2000 chars** of the transcript. May be empty if `status !== "Done"` or for sources without a transcript yet.                     |
| `textPreviewTruncated` | boolean           | `true` if the original transcript exceeded 2000 chars.                                                                                   |
| `hasFullText`          | boolean           | `true` if a transcript exists at all (regardless of whether the preview is truncated). Use this to know whether processing has finished. |

The S3 storage keys, internal upload identifiers, the original
processing-flusher state, and the secret presigned URLs are
intentionally **not** in this response. We never return them through the
public API.

### Truncation contract

* `textPreviewTruncated === true` → the preview is exactly 2000
  characters and there is more transcript on the server.
* `textPreviewTruncated === false` AND `hasFullText === true` → the
  preview is the full transcript.
* `hasFullText === false` → the source has no transcript yet (still
  processing or processing failed). `textPreview` will be empty.

We **do not** truncate at character boundaries that could split UTF-8
codepoints in the middle of a multi-byte sequence — the implementation
slices safely at the codepoint level. Your client can render the preview
without sanitisation.

### Errors

| Status | Code                                                                                     |
| ------ | ---------------------------------------------------------------------------------------- |
| 401    | `unauthenticated`, `invalid_token`, `key_revoked`, `key_expired`                         |
| 403    | `scope_missing` (need `notes:read` — sources share the notes scope), `plan_not_eligible` |
| 404    | `not_found`                                                                              |
| 429    | `rate_limited`                                                                           |

***

## `POST /v1/sources`

Create a source. Required scope: `sources:write`.

Two input shapes:

* `type: "text"` — synchronous. The caller supplies the prepared
  transcript and the row is stored with `status: Success` immediately.
  Returns the new `Source` envelope (200).
* `type: "file"` — asynchronous. The caller first calls
  [`POST /v1/uploads`](./uploads.md) to obtain a presigned S3 PUT URL +
  opaque `uploadId` handle, PUTs the bytes, then references the handle
  here. Returns a `Job` envelope (200) with `type: SOURCE_INGEST_FILE`;
  poll `GET /v1/jobs/{jobId}` until `status: COMPLETED` and the
  resulting `result.sourceId` becomes available.

```bash theme={null}
curl -i https://api.scripe.io/v1/sources \
  -X POST \
  -H "Authorization: Bearer scripe_sk_live_…" \
  -H "Scripe-Api-Version: 2026-08-01" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-id-from-your-side" \
  -d '{
    "type": "text",
    "projectId": "proj_01J9ZA…",
    "text": "Today we shipped the new pricing model…",
    "name": "Stand-up summary 2026-05-15"
  }'
```

### Request body — `type: "text"`

| Field       | Type              | Required | Notes                                                            |
| ----------- | ----------------- | -------- | ---------------------------------------------------------------- |
| `type`      | string            | yes      | `"text"`.                                                        |
| `projectId` | string (`proj_*`) | yes      | Must belong to the workspace; otherwise `404 not_found`.         |
| `text`      | string            | yes      | Up to 1 MB. Empty string is rejected with `400 invalid_request`. |
| `name`      | string \| null    | no       | Optional human label.                                            |

### Request body — `type: "file"`

| Field       | Type              | Required | Notes                                                                                                                           |
| ----------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `type`      | string            | yes      | `"file"`.                                                                                                                       |
| `projectId` | string (`proj_*`) | yes      | Must belong to the workspace; otherwise `404 not_found`.                                                                        |
| `uploadId`  | string (`upl_*`)  | yes      | Returned by `POST /v1/uploads`. The handle is bound to the workspace; using one from another workspace returns `404 not_found`. |
| `name`      | string \| null    | no       | Optional human label. Defaults to the bucket key tail.                                                                          |

### Response — `type: "text"`

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json
Idempotent-Replayed: false

{
  "data": {
    "id": "src_01J9ZA…",
    "projectId": "proj_01J9ZA…",
    "status": "Success",
    "name": "Stand-up summary 2026-05-15",
    "fileType": null,
    "durationSeconds": 0,
    "createdAt": "2026-05-15T08:21:00.000Z",
    "updatedAt": null,
    "textPreview": "Today we shipped the new pricing model…",
    "textPreviewTruncated": false,
    "hasFullText": true
  }
}
```

### Response — `type: "file"`

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json
Idempotent-Replayed: false

{
  "data": {
    "id": "job_01J9ZB…",
    "type": "SOURCE_INGEST_FILE",
    "status": "QUEUED",
    "projectId": "proj_01J9ZA…",
    "startedAt": null,
    "completedAt": null,
    "progress": null,
    "result": null,
    "errorCode": null,
    "errorMessage": null,
    "attemptCount": 0,
    "estimatedCompletionMs": 240000,
    "createdAt": "2026-05-15T08:21:00.000Z",
    "updatedAt": null
  }
}
```

Once the worker finishes:

```json theme={null}
{
  "id": "job_01J9ZB…",
  "status": "COMPLETED",
  "result": { "sourceId": "src_01J9ZA…" },
  ...
}
```

`GET /v1/sources/{sourceId}` then returns the fully-processed source.

The async path also enforces the per-plan **AI spend cap**; calls that
would push the daily cost over the cap return `402 spend_cap_exceeded`
without enqueueing a job. See [`docs/api/v1/jobs.md`](./jobs.md) for
the full lifecycle and pricing.

### Errors

| Status | Code                                                                                                |
| ------ | --------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request` (missing `projectId`/`type`, empty `text`, missing `uploadId` for `type: "file"`) |
| 401    | `unauthenticated`, `invalid_token`, `key_revoked`, `key_expired`                                    |
| 402    | `spend_cap_exceeded` (file branch only — daily AI spend cap reached)                                |
| 403    | `scope_missing` (need `sources:write`), `plan_not_eligible`                                         |
| 404    | `not_found` (project not in workspace; or `uploadId` belongs to another workspace)                  |
| 409    | `idempotency_key_conflict`                                                                          |
| 413    | `payload_too_large` (`text` > 1 MB)                                                                 |
| 422    | `unprocessable` (e.g. unknown `type` value)                                                         |
| 429    | `rate_limited`                                                                                      |

***

## What's NOT here (yet)

* **List endpoint.** No `GET /v1/sources`. Plan-side decision; if you
  have a use case, file a feature request.
* **Full transcript download.** See above.
* **File / audio download.** Storage URLs are private. The dashboard's
  download links are signed and time-limited; we won't expose them via
  the API in v1.
* **Update / delete endpoints.** `PATCH` / `DELETE` are out of scope
  for v1.
