Contents
The Content resource in the inklet SDK — upload assets to storage, refresh upload tickets, and read what a Content became.
A Content is one submission: a set of assets, and nothing more. It is storage, not work. Uploading spends no quota and runs no AI — nothing happens to a Content until an Analysis references it.
const { content } = await inklet.contents.upload({
title: "Dentist",
assets: [inklet.assets.text("Dentist at 9am tomorrow")],
});upload()
The one call most applications need. It creates the Content, uploads every binary asset directly to its presigned URL, and retries a failed upload once with a fresh ticket.
upload(input: UploadContentInput): Promise<UploadContentResult>Prop
Type
Prop
Type
The returned idempotencyKey is worth keeping: push.* and
presentations.generate() reuse the same key for the Content and the Analysis
they start from it.
Content states
pending ──→ ready
└─→ failed| State | Meaning |
|---|---|
pending | Assets are not all confirmed in storage yet. |
ready | Every asset is stored. |
failed | Ingestion gave up; failure explains why. |
Text and link Contents are ready immediately. A Content with binary assets is
pending until inklet has verified the uploads, which happens on its own or
when an Analysis first references the Content.
You rarely need to wait for ready. inklet.analyze() verifies pending
uploads itself, so uploading and analyzing back to back is fine. Use
waitUntilReady() only when you want to observe ingestion without analyzing.
waitUntilReady()
const stored = await inklet.contents.waitUntilReady(content);Polls until the Content is ready or failed. pollIntervalMs defaults to
1,000 ms (100–60,000) and timeoutMs to 60,000 ms (1–1,800,000); a signal
aborts with OperationAbortedError, and running out of time throws
OperationTimeoutError.
retrieve() and list()
const content = await inklet.contents.retrieve("content_123");
const page = await inklet.contents.list({ state: "ready", limit: 20 });Prop
Type
An out-of-range state, or a limit outside 1–50, throws
ConfigurationError locally.
Searching
q searches the Content library:
const found = await inklet.contents.list({ q: "dentist thursday" });Every whitespace-separated term must appear on the same Content — in its title, or in an asset's text, link, or filename, or in what inklet read out of an image or file. Matching is case-insensitive and by substring, so Chinese, Japanese, and Korean text matches without word breaks. Paging and ordering are the same as for the unfiltered list, and searching is free on every plan.
Text, links, and filenames are searchable the moment a Content is created. What inklet reads out of an image or file is filled in shortly afterwards, so a search by what a new upload shows can take a moment to find it.
The SDK trims q and sends nothing for a blank value. More than 200 characters
(MAX_CONTENT_SEARCH_LENGTH) throws ConfigurationError before a request is
made.
create()
The lower level, for applications that manage their own uploads.
create(input: CreateContentRequest, idempotencyKey: string): Promise<CreateContentResponse>Prop
Type
The asset shape here differs from inklet.assets.*. Binary
assets are declared by metadata only — filename, contentType,
sizeBytes — because the bytes go to a presigned URL afterwards, not in this
request body.
type CreateContentAssetInput =
| { type: "text"; text: string }
| { type: "link"; url: string }
| { type: "image"; filename: string; contentType: AllowedImageContentType; sizeBytes: number }
| { type: "file"; filename: string; contentType: AllowedFileContentType; sizeBytes: number };Returns the Content plus one upload ticket per binary asset:
Prop
Type
Prop
Type
idempotencyKey is required here — 8–128 printable ASCII characters, no
spaces. It is sent as the idempotency-key header.
Validation
Enforced locally, before the request:
| Field | Rule |
|---|---|
title | A string or null |
assets | 1–50 entries |
sizeBytes | Integer, 1 to 10 MiB |
| Link URLs | Absolute HTTP(S), no embedded credentials |
| Text | At least one non-whitespace character |
contentType | Within the allowed list for that asset kind |
refreshUploadTickets()
New presigned URLs for assets whose tickets expired or whose upload failed:
const refreshed = await inklet.contents.refreshUploadTickets(content.id, [0, 2]);assetIndexes must be one or more unique non-negative integers; anything else
throws ConfigurationError. Returns the same shape as create().
This also accepts a failed Content. A Content that failed because its
upload window closed (failure.code is upload_expired) goes back to
pending once new tickets are issued, so missing the window once is not
permanent. A ready Content keeps its state.
The Content type
Prop
Type
Prop
Type
analysisIds counts only Analyses that named this Content. An Analysis
that picked it up from history as context does not appear there — that
relationship is recorded on the Presentation instead, as a context role. See
Presentations.
Doing it by hand
The full sequence upload() performs, if you need to own each step:
const created = await inklet.contents.create(
{
title: "Menu",
assets: [
{ type: "text", text: "Tonight" },
{
type: "file",
filename: "menu.pdf",
contentType: "application/pdf",
sizeBytes: bytes.byteLength,
},
],
},
"menu-2026-08-15",
);
for (const ticket of created.uploadTickets) {
const form = new FormData();
for (const [key, value] of Object.entries(ticket.fields)) {
form.append(key, value);
}
form.append("file", new Blob([bytes], { type: "application/pdf" }), "menu.pdf");
await fetch(ticket.url, { method: "POST", body: form });
}
const analysis = await inklet.analyze({ contentIds: [created.content.id] });Note the upload fetch above carries no inklet credentials — that is
deliberate, and upload() does the same. Never attach your PAT to a storage
URL.
When an upload will not land
If assets are still missing after the SDK has refreshed their tickets and retried, it throws:
import { AssetUploadError } from "@inklethq/sdk";
try {
await inklet.contents.upload({ assets });
} catch (error) {
if (error instanceof AssetUploadError) {
console.error(error.contentId, error.failedAssetIndexes, error.cause);
}
}failedAssetIndexes maps back to the positions in the array you passed, and
cause is the underlying failure of the first asset that still failed. Like
every error from a call that sent an Idempotency-Key, it carries
idempotencyKey: call upload() again with it to pick the same Content back
up instead of creating a second one.