# Getting started (/getting-started)



<div className="fd-steps [&_h3]:fd-step">
  ### Install [#install]

  <Tabs items="[&#x22;npm&#x22;, &#x22;pnpm&#x22;, &#x22;yarn&#x22;, &#x22;bun&#x22;]">
    <Tab value="npm">
      ```sh
      npm install @inklethq/sdk
      ```
    </Tab>

    <Tab value="pnpm">
      ```sh
      pnpm add @inklethq/sdk
      ```
    </Tab>

    <Tab value="yarn">
      ```sh
      yarn add @inklethq/sdk
      ```
    </Tab>

    <Tab value="bun">
      ```sh
      bun add @inklethq/sdk
      ```
    </Tab>
  </Tabs>

  Node.js 20 or newer is required.

  ### Create a token [#create-a-token]

  Personal access tokens are issued in the Portal dashboard. Keep the token in
  your environment, never in source control:

  ```sh filename=".env"
  INKLET_PAT=your-token-here
  ```

  ### Initialize the client [#initialize-the-client]

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

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

  Construction is side-effect free — configuration is validated, but no request
  is made until you call something. CommonJS works too:

  ```js
  const { Inklet } = require("@inklethq/sdk");
  ```

  ### Find a display [#find-a-display]

  ```ts
  const page = await inklet.displays.list({ limit: 20 });

  for (const display of page.items) {
    console.log(display.id, display.name, display.online);
  }
  ```

  ### Push something [#push-something]

  Auto Push uses inklet AI and requires Pro. Free accounts can follow the same
  setup with [Hardcode Push](/push/hardcode), which accepts a finished PNG or
  JPEG without AI processing.

  ```ts
  const result = await inklet.push.auto({
    title: "Grocery list",
    assets: [inklet.assets.text("Milk, eggs, coffee")],
  });

  console.log(result.contentId, result.state);
  ```

  ### Wait for it to land [#wait-for-it-to-land]

  The call above returns as soon as inklet has your assets — usually with
  `state: "processing"` and no Presentation IDs yet. Poll until it settles:

  ```ts
  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);
  } else {
    console.log("presentations:", content.presentationIds);
  }
  ```
</div>

## A complete example [#a-complete-example]

```ts filename="brief.ts"
import { Inklet, InkletError } from "@inklethq/sdk";
import { readFile } from "node:fs/promises";

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

async function main() {
  const chart = inklet.assets.image({
    data: await readFile("chart.png"),
    filename: "chart.png",
    contentType: "image/png",
  });

  const result = await inklet.push.auto({
    idempotencyKey: `daily-brief-${new Date().toISOString().slice(0, 10)}`,
    title: "Daily brief",
    intent: "Lead with the number, keep the chart secondary",
    assets: [
      inklet.assets.text("Revenue is up 12% week over week."),
      chart,
    ],
  });

  console.log(`content ${result.contentId} (${result.state})`);
}

main().catch((error) => {
  if (error instanceof InkletError) {
    console.error(error.code, error.status, error.requestId);
  }
  process.exitCode = 1;
});
```

<Callout>
  The idempotency key above is derived from the date, so re-running the script
  on the same day replays the same push rather than creating a second one. See
  [Idempotency](/lifecycle#idempotency).
</Callout>

## Next [#next]

* [Authentication](/authentication) — tokens, environments, and pointing at a Compute Hub
* [Plans and AI features](/plans) — Free vs. Pro and subscription authorization
* [Pushing content](/push) — the differences between Auto, Manual, and Hardcode
* [Lifecycle](/lifecycle) — states, stages, and knowing when a frame is real
