Developers

inklet Portal SDK

A server-side TypeScript client for the paper on your walls. Hand it text, a link, an image, or a PDF — inklet does the layout, picks the room, and renders for the panel. Follow the agent while it works, or skip the AI and put a picture up as-is.

$npm install @inklethq/sdk

v0.2.2 · Node 20+ · ESM & CommonJS

brief.ts
import { Inklet } from "@inklethq/sdk";

const inklet = new Inklet({ pat: process.env.INKLET_PAT! });

const { analysis } = await inklet.push.auto({
  title: "Daily brief",
  intent: "Make the key update easy to scan",
  assets: [
    inklet.assets.text("Revenue is up 12% week over week."),
    inklet.assets.link("https://example.com/report"),
  ],
});

const done = await inklet.analyses.wait(analysis);
console.log(done.presentationIds);

Push

Three ways to put something on a wall.

They differ in how much of the decision you keep. Each is one call over an upload and an analysis, hands back the Content and the Analysis it started, and can be replayed safely under one idempotency key. Auto and Manual run inklet's agent and need a Pro plan; Hardcode runs no AI and stays on Free.

01Auto

You have something worth showing and no opinion about where.

inklet reads the assets, chooses the displays that can render them, and does the typesetting. Intent is a sentence of direction, not a template — it steers the layout without describing it. Add context: "history" and the agent may also read what you sent before.

const result = await inklet.push.auto({
  idempotencyKey: "daily-brief-2026-09-15",
  title: "Daily brief",
  intent: "Make the key update easy to scan",
  context: "history", // optional: read earlier uploads too
  assets: [
    inklet.assets.text("Revenue is up 12% week over week."),
    inklet.assets.link("https://example.com/report"),
  ],
});
02Manual

You know the room. inklet still sets the type.

One display, named by id. The agent still summarises and lays out — text, links, images, PDFs — but the routing decision stays yours.

await inklet.push.manual({
  displayId: "display_123",
  assets: [
    inklet.assets.image({
      data: await readFile("chart.png"),
      filename: "chart.png",
      contentType: "image/png",
    }),
    inklet.assets.text("This week's trend"),
  ],
});
03Hardcode

You already made the picture.

Exactly one PNG or JPEG, to exactly one display, rendered as sent. No AI runs and no AI allowance is spent. inklet scales it to the panel — your source does not have to arrive at 800×480.

await inklet.push.hardcode({
  displayId: "display_123",
  image: inklet.assets.image({
    data: await readFile("poster.jpg"),
    filename: "poster.jpg",
    contentType: "image/jpeg",
  }),
});

Pipeline

A Content is what you sent. An Analysis is what inklet did with it.

Uploading and analyzing are separate steps. A Content is just the stored assets: no AI runs and nothing is spent until an Analysis references it. The Analysis produces Presentations, and a display shows one once it wakes and confirms it. Waiting returns when the run is over, or throws with the backend's code when it failed.

ContentWhat you handed in.
pendingready
AnalysisOne run of the agent.
queuedrunningcompleted
PresentationWhat a specific panel will show.
preparingqueuedpublishedconfirmedexpired

analyze() options

  • contentIdsContents to analyze. Omit to summarise recent history instead.
  • contextsubmitted, the default, or history: the agent may also read your earlier uploads.
  • scope{ since: "72h" } — how far back history reaches. The plan clamps it; sinceAt says where it landed.
  • targetOmit to let the agent choose, name displayIds to pin, or { output } for a software-only Scene.
  • intent · titleA sentence of direction for the agent. title overrides the generated one.
analyze.ts
// 1. Store Content. No AI runs, nothing is spent.
const { content } = await inklet.contents.upload({
  title: "Dentist",
  assets: [inklet.assets.text("Dentist at 9am tomorrow")],
});

// 2. Analyze it. inklet picks compatible Displays.
const analysis = await inklet.analyze({
  contentIds: [content.id],
  intent: "Make a reminder card",
});

// 3. Wait for the run. A history-only run can queue
//    for a while, so raise timeoutMs for those.
const done = await inklet.analyses.wait(analysis);

if (done.outcome === "presentations") {
  console.log(done.presentationIds);
} else {
  console.log("no change:", done.noChangeReason);
}

Events

Watch the agent work, as it works.

An Analysis publishes an ordered event stream: what the agent was given, what it is reading, what it planned, and how the result was rendered and delivered. It streams over server-sent events, resumes from the last event if the connection drops, falls back to polling behind a proxy that cannot stream, and ends on its own when the run does. Every event becomes one English line — the same line the Portal shows.

  • Acceptedanalysis.createdanalysis.dispatchedanalysis.leasedanalysis.lease_expired
  • Workingcontext.materializedagent.activity
  • Planningplan.submittedplan.rejectedplan.accepted
  • Resultrender.finishedrender.faileddelivery.publisheddelivery.confirmeddelivery.failed
  • Finishedanalysis.completedanalysis.failed

A progress report, not the run's log. The agent's own working notes — each tool call, its arguments, the model's text — are not part of the public API at any depth, so a UI built on the stream survives a release that changes nothing you can see. The Result row lands after the run is already over, days later for a sleeping panel; read it back from the timeline.

watch.ts
import { describeEvent, isAnalysisEvent } from "@inklethq/sdk";

for await (const ev of inklet.analyses.watch(analysis.id)) {
  console.log(describeEvent(ev));
  // Reading your notes · 3 read
  // Looked at 2 layouts · chose Daily Summary
  // Submitted the plan · 2 actions

  if (isAnalysisEvent(ev, "plan.accepted")) {
    console.log(ev.data.presentationIds);
  }
}

// Rendering and delivery are written after the run
// ends, so watch() never sees them. timeline() does.
for await (const ev of inklet.analyses.timeline(analysis.id)) {
  if (ev.level !== "info") console.warn(ev.summary);
}

Control

Two things a push cannot do.

Neither of these runs the agent over your Contents again, and neither spends an AI allowance.

Switch the image on a display

Pick what is on the panel by hand. Any Presentation that has already been delivered there and rendered can go back up — including one that has expired — or you can simply move to the next queued one. Both land as pending until the panel confirms, and a panel's full history is one list call away.

switch.ts
// Everything this panel has shown before, newest first.
const shown = await inklet.presentations.list({
  displayId,
  state: "expired",
});

// Put an earlier card back. It confirms on its next sync.
const previous = shown.items[0].id;
await inklet.displays.setCurrent(displayId, previous);
await inklet.displays.waitUntilCurrent(displayId, previous);

// Or just move on to whatever is queued next.
const { changed } = await inklet.displays.advance(displayId);

Generate a Presentation without a display

For software-only surfaces — a widget, a screensaver, a test. Generating is an upload plus an analysis with an output target: it registers nothing, queues nothing, and produces versioned Scene JSON with PNG renditions. A stored Scene renders again at another size without a second AI run.

generate.ts
const generation = await inklet.presentations.generate({
  intent: "Create a calm, glanceable summary",
  assets: [
    inklet.assets.text("Revenue increased 12% this week."),
  ],
  output: {
    preset: "macos-widget-medium",
    formats: ["scene", "png"],
  },
});

const presentation =
  await inklet.presentations.waitUntilReady(generation);
console.log(presentation.scene?.data);
console.log(presentation.renditions[0]?.url);

// Another size from the same Scene. No second AI run.
await inklet.presentations.render(presentation.id, {
  viewport: { width: 720, height: 340 },
});

Reference

Every call, documented.

Browse the HTTP API reference

The SDK is a thin, typed layer over the HTTP API — one method per endpoint, nothing hidden. Parameters, response shapes, error codes, and a TypeScript SDK snippet for every call live in the reference.

Guardrails

A key that reaches your walls deserves care.

Server-only, by construction

Constructing the client where a document exists throws before a request is made. A personal access token cannot end up in a browser bundle by accident.

Uploads never carry the token

Binary assets go straight to temporary storage URLs. The token is sent only to inklet endpoints, and requests refuse absolute URLs and cross-origin redirects.

Safe to replay

Every Content and every Analysis is created under an idempotency key — yours, or one the SDK generates and hands back. Replaying a key returns the original resource, and the same key with a different body is a conflict, so a retry is the same call, never a second one.

Errors you can act on

Every error extends InkletError and keeps the backend code, HTTP status, request ID, and structured details. The class comes from the status; the code and details are the stable parts. Messages are for logs — never branch on them.

errors.ts
import {
  InkletError,
  PermissionDeniedError,
  RateLimitError,
} from "@inklethq/sdk";

try {
  await inklet.analyze({ contentIds: [content.id] });
} catch (error) {
  if (
    error instanceof PermissionDeniedError &&
    error.code === "plan_upgrade_required"
  ) {
    // A Pro-only run on the Free plan. Upgrade, then retry.
  } else if (error instanceof RateLimitError) {
    // quota_exceeded clears at details.resetAt;
    // rate_limited clears with back-off.
  } else if (error instanceof InkletError) {
    console.error(error.code, error.requestId, error.details);
  }
}

Or keep it off the cloud entirely

The service address defaults to dev.iminklet.com while the SDK is in developer preview. Point baseUrl at a Compute Hub instead and the same code runs without anything leaving your network.

10 MiB

per binary asset

50

assets per Content

16

event types, a closed set

Start with a token.

The SDK is at 0.2 and still in developer preview. The surface is small on purpose and stable enough to build on; breaking changes are listed in the changelog with what to change.

01

Create a token

Personal access tokens are issued under API tokens in the Portal, with a name and an optional expiry. Each is shown once — copy it into your environment, never into source.

02

Install the package

npm install @inklethq/sdk — Node 20 or newer, ESM or CommonJS, types included.

03

Push something

One call puts words on a wall. Then watch the run, read a panel's history, or skip the agent and switch the image yourself.