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
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.
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/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 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.
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/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/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:
{
"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 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.