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.

$npm install @inklethq/sdk

v0.1.0 · Node 20+ · ESM & CommonJS

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

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

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"),
  ],
});

Push

Three ways to put something on a wall.

They differ in how much of the decision you keep. All three take the same assets, return the same result, and can be replayed safely with an idempotency key.

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.

const result = await inklet.push.auto({
  idempotencyKey: "daily-brief-2026-08-15",
  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"),
  ],
});
02Manual

You know the room. Inklet still sets the type.

One display, named by id. The asset pipeline is unchanged — text, links, images, PDFs all still get summarised and laid out — 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. 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",
  }),
});

Lifecycle

A push is a request, not a render.

The call returns as soon as Inklet has your assets. Summarising, routing, and rendering happen after — and a display only shows the result once it wakes and confirms it. Poll the Content if you need to know it landed.

ContentWhat you handed in.
pendingprocessingready
PresentationWhat a specific panel will show.
preparingqueuedpublishedconfirmed

Processing stages

awaiting_upload · fetching_links · summarizing · routing · creating_presentations

wait.ts
const result = await inklet.push.auto({ assets });

// A successful push commonly returns a `processing` Content
// with no Presentation IDs yet. Poll until it settles.
let content = await inklet.contents.retrieve(result.contentId);

while (content.state === "processing") {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  content = await inklet.contents.retrieve(content.id);
}

if (content.state === "failed") {
  console.error(content.processing.error);
}

Reference

Four resources, fully typed.

inklet.displays

The panels bound to your account, and what each can render.

  • list()Paginated, newest first
  • retrieve(id)Battery, firmware, tags, capabilities
  • listQueue(id)What is waiting, over a time range
  • current(id)The confirmed Presentation, or null

inklet.contents

The lower-level lifecycle, when push.* hides too much.

  • create(input, key)Returns upload tickets
  • retrieve(id)State, stage, warnings, errors
  • list()Filter by mode and state
  • confirm(id)Close uploads and start processing

inklet.presentations

A rendered frame for one display, in one format.

  • retrieve(id)png · raw2 · raw4

inklet.assets

Validated locally, before anything leaves the process.

  • text(string)Plain prose
  • link(url)Fetched and summarised by Inklet
  • image({ data })png · jpeg · gif · webp · svg
  • file({ data })pdf · txt · md · json

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 push takes an idempotency key. Omit it and the SDK generates one, then hands it back — so your retry is the same call, not a second one.

Errors you can act on

Every error extends InkletError and keeps the backend code, HTTP status, request ID, and structured details. Credentials are redacted from messages.

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

try {
  await inklet.displays.list();
} catch (error) {
  if (error instanceof RateLimitError) {
    // Retry on your own schedule.
  } else if (error instanceof InkletError) {
    console.error(error.code, error.status, error.requestId);
  }
}

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 push

3

output formats

Start with a token.

The SDK is in developer preview. The surface is small on purpose and stable enough to build on; breaking changes are announced in the changelog.

01

Create a token

Personal access tokens are issued in the Portal dashboard and scoped to your displays.

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. Everything else is refinement.