inklet docs

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
StateMeaning
pendingAssets are not all confirmed in storage yet.
readyEvery asset is stored.
failedIngestion 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:

FieldRule
titleA string or null
assets1–50 entries
sizeBytesInteger, 1 to 10 MiB
Link URLsAbsolute HTTP(S), no embedded credentials
TextAt least one non-whitespace character
contentTypeWithin 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.

On this page