Skip to content

Targetless Presentations

A Targetless Presentation lets an authenticated user generate visual content without owning or registering any inklet Display. The result is a versioned inklet Scene v1 JSON document and zero or more PNG renditions.

Key constraint: Generation never creates a Device row, publishes a Push, sends MQTT, or requires a compatible Display.


Authentication

Targetless Presentations are mounted on two prefixes backed by the same business logic:

Prefix Auth Consumer
/api/sdk/v1 PAT (Bearer il_pat_...) SDK, server integrations
/api/app/v1 User access token (JWT) macOS / iOS / Portal

/api/app/v1 issues X-Renewed-Token for sliding renewal; /api/sdk/v1 never does.


Permissions

Mode Free Pro
mode=auto (AI generation) 403 plan_upgrade_required Allowed
mode=hardcode (direct image) Allowed Allowed

Downgraded users can still read existing Presentations, list them, renew URLs, and add rendition sizes to an existing Scene — none of these re-invoke AI or hit the Pro gate.


Creating Targetless Content

Add an output field to POST /contents. Its presence routes processing to the targetless branch instead of Display delivery.

POST /api/sdk/v1/contents
Idempotency-Key: <8-128 printable ASCII>
Content-Type: application/json
Authorization: Bearer il_pat_...
{
  "mode": "auto",
  "intent": "Create a calm, glanceable weekly summary",
  "title": "Weekly Summary",
  "output": {
    "preset": "macos-widget-medium",
    "formats": ["scene", "png"],
    "colorMode": "color"
  },
  "assets": [
    { "type": "text", "text": "Revenue increased 12% this week." }
  ]
}

output fields

Field Type Description
preset string? Named preset; mutually exclusive with viewport
viewport object? { "width": N, "height": N }; mutually exclusive with preset
formats string[] ["scene", "png"]; defaults to ["scene", "png"]
colorMode string color / grayscale / monochrome; default color

If neither preset nor viewport is given, the default 800×480 preview profile is used. Viewport width and height are integers in 1...8192, and the total pixel count must not exceed 4,000,000.

Preset registry

Preset Viewport Default colorMode
default 800×480 color
macos-widget-small 170×170 color
macos-widget-medium 360×170 color
macos-widget-large 360×376 color

Per-mode rules

Mode displayId output Meaning
auto Forbidden (null or omitted) Required AI generation, targetless
hardcode Forbidden (null or omitted) Required Direct image, no AI
manual Required Forbidden Display-bound, not targetless

manual + output and output + displayId (non-null) both return 400 invalid_request and are additionally enforced by DB CHECK constraints.

Normalized Content output field

{
  "output": {
    "formats": ["scene", "png"],
    "preset": "macos-widget-medium",
    "viewport": { "width": 360, "height": 170 },
    "colorMode": "color"
  }
}

Legacy Display-bound Content returns "output": null.


Processing

Auto targetless

confirm → job → verify assets → fetch links → AI analysis →
  build Scene v1 → persist Presentation + Content link →
    Content marked ready → enqueue PNG renditions

Never enters Display routing, never creates a Push row, never sends MQTT. Scene generation may finish before PNG rendering; in that case the Presentation is preparing while the Content is already ready (Presentation ID is durable).

Hardcode targetless

confirm → job → build Scene (viewport fill, fit: "stretch") →
  persist Presentation → enqueue PNG renditions

Accepts exactly one PNG or JPEG. Runs no AI analysis, summarization, or template selection.


Scene v1

Media type: application/vnd.inklet.scene+json;version=1

{
  "scene": {
    "mediaType": "application/vnd.inklet.scene+json;version=1",
    "version": 1,
    "data": {
      "version": 1,
      "viewport": { "width": 360, "height": 170 },
      "background": "#ffffff",
      "elements": [
        {
          "id": "headline",
          "type": "text",
          "frame": { "x": 20, "y": 20, "width": 320, "height": 80 },
          "properties": {
            "text": "Revenue up 12%",
            "fontSize": 28,
            "fontWeight": 600,
            "color": "#000000",
            "align": "leading"
          }
        }
      ]
    }
  }
}

Element types

text

Property Type Default Notes
text string Required
fontSize number 16 pt
fontWeight number 400 < 600 Regular; ≥ 600 Bold
color string #000000 Hex colour
align string leading leading / center / trailing

Overflow is clipped and appended with an ellipsis. CJK code points use Noto CJK; other scripts use DejaVu.

image

Property Type Default Notes
fileId string Required; owned asset file ID
fit string contain contain / cover / fill / stretch

API responses replace fileId with a 15-minute signed read URL (url field). The persisted Scene stores only fileId — no credentials or permanent signed URLs.

shape

Property Type Default Notes
shapeType string rect rect / ellipse
fill string #000000 Hex fill colour
cornerRadius number 0 Pixels; rect only
strokeColor string? null Hex stroke colour
strokeWidth number 0 Pixels

Common rules

  • Frame coordinates are integers in the Scene viewport coordinate space.
  • width and height must be positive.
  • Element id values are unique within one Scene.
  • Stacking order = document order (later elements paint over earlier ones; no z-index).
  • Out-of-bounds frames are clipped; alpha is composited onto a white background.
  • Invalid values (out-of-enum, wrong type) degrade to the default; invalid structure (missing required field) returns 400.
  • Unknown optional properties are preserved by storage and ignored by renderers; unknown element types are skipped.
  • Scenes must not contain PATs, user access tokens, presigned upload policies, private object keys, or permanent third-party credentials.

Presentation response model

{
  "id": "019...",
  "displayId": null,
  "contentIds": ["019..."],
  "mode": "auto",
  "state": "ready",
  "output": {
    "formats": ["scene", "png"],
    "preset": "macos-widget-medium",
    "viewport": { "width": 360, "height": 170 },
    "colorMode": "color"
  },
  "scene": {
    "mediaType": "application/vnd.inklet.scene+json;version=1",
    "version": 1,
    "data": { "version": 1, "viewport": { "width": 360, "height": 170 }, "background": "#ffffff", "elements": [] }
  },
  "renditions": [
    {
      "id": "019...",
      "mediaType": "image/png",
      "format": "png",
      "width": 360,
      "height": 170,
      "colorMode": "color",
      "state": "ready",
      "url": "https://...",
      "expiresAt": "2026-09-01T20:15:00Z",
      "failure": null,
      "updatedAt": "2026-09-01T20:00:00Z"
    }
  ],
  "image": null,
  "failure": null,
  "createdAt": "2026-09-01T20:00:00Z",
  "updatedAt": "2026-09-01T20:01:00Z"
}

Targetless vs Display Presentation differences

Field Targetless Display
displayId null UUID string
scene Scene v1 object null
renditions list of renditions []
image null rendered image object
state preparing / ready / failed preparing / queued / published / confirmed / expired / failed

Presentation aggregate state

Only the initial renditions (from Content output.formats) affect Presentation state:

State Meaning
preparing Scene persisted; initial renditions not yet complete
ready All initial renditions are ready or failed
failed Scene generation itself failed (terminal)

Renditions added later with POST /presentations/{id}/renditions never change the Presentation's aggregate state.

Rendition states

state url / expiresAt failure
preparing null null
ready Signed URL + expiry null
failed null structured problem
  • Signed URL lifetime is ~15 minutes; re-reading the Presentation re-signs the same stored file — no re-render.
  • A failed rendition remains queryable (not a 404).
  • A single rendition failure does not affect the Presentation or any other rendition.

API endpoints

GET /presentations

GET /api/sdk/v1/presentations?scope=generated&state=ready&limit=20
GET /api/app/v1/presentations?scope=generated&state=ready&limit=20
Parameter Values Default
scope generated (targetless) / display / all generated
state preparing / ready / failed all
limit 1–50 20

Returns the standard cursor page envelope { items, nextCursor, hasMore }.

GET /presentations/{id}

GET /api/sdk/v1/presentations/{presentationId}
GET /api/app/v1/presentations/{presentationId}

Pure read. May re-sign rendition URLs and Scene image asset URLs; never renders, publishes, enqueues, or wakes a Display.

  • Presentation not yours → 404 presentation_not_found

POST /presentations/{id}/renditions

Append a new PNG size to an existing Presentation. Reuses the persisted Scene; never reruns AI.

POST /api/sdk/v1/presentations/{presentationId}/renditions
POST /api/app/v1/presentations/{presentationId}/renditions
Idempotency-Key: optional-but-recommended
{
  "formats": ["png"],
  "viewport": { "width": 720, "height": 340 },
  "colorMode": "color"
}

Dedup key: (presentation_id, format, width, height, color_mode). Concurrent identical requests return the same rendition — no duplicate render work.

Response 202 (async):

{
  "id": "019...",
  "format": "png",
  "width": 720,
  "height": 340,
  "colorMode": "color",
  "state": "preparing",
  "url": null,
  "expiresAt": null,
  "failure": null,
  "updatedAt": "2026-09-01T20:05:00Z"
}

Poll GET /presentations/{id} for completion.


PNG rendering scale rules

Case Rule Outcome
Requested size is same aspect ratio as Scene viewport Uniform scale Exact proportional scale, zero letterbox (covers Retina/HiDPI)
Requested size is different aspect ratio min(sx, sy) scale, centred Scene background colour fills the letterbox; no distortion, no crop
Hardcode mode Direct anisotropic stretch Fills output exactly; no aspect-ratio preservation, no letterbox, no crop

Same-ratio detection uses integer cross-multiplication (not float comparison) to prevent a spurious one-pixel border on Retina requests.


Resource limits

Limit v0.1 default Env var Over-limit code
Pixels per rendition 4,000,000 TARGETLESS_MAX_RENDITION_PIXELS 400 invalid_request
Renditions per Presentation 20 TARGETLESS_MAX_RENDITIONS_PER_PRESENTATION 409 invalid_state
Concurrent preparing per user 8 TARGETLESS_MAX_CONCURRENT_RENDITIONS 429 rate_limited + Retry-After
  • The pixel limit is enforced at both Content creation and POST /renditions.
  • The concurrency limit does not apply to duplicate requests that hit the dedup key (rejecting them would break idempotency).
  • A value of 0 or unset falls back to the safe default — it is not treated as unlimited.

macOS app acceptance path

The macOS app uses /api/app/v1 with its existing access token:

  1. POST /api/app/v1/contents with output.preset=macos-widget-medium (or another preset)
  2. Upload binary assets to the returned presigned tickets (no Authorization header)
  3. POST /api/app/v1/contents/{id}/confirm
  4. Poll GET /api/app/v1/contents/{id} until state=ready or state=failed
  5. Retrieve presentationIds[0]
  6. Download the PNG rendition that best matches the current Widget family size
  7. Write the PNG and Scene JSON to the shared Widget App Group directory
  8. Reload the Widget timeline

The Widget itself never holds a PAT or access token and never relies on a signed URL staying valid. The host app owns network access and writes durable local cache files for WidgetKit.


Error reference

HTTP Code When
400 invalid_request Invalid output fields; pixel limit exceeded
402 payment_required Subscription billing failed
403 plan_upgrade_required Free user requesting mode=auto targetless
404 presentation_not_found Presentation not found or not yours
409 invalid_state Rendition count limit reached for this Presentation (permanent)
429 rate_limited Concurrent rendition limit; Retry-After header included

Permission error codes

This implementation uses the existing entitlement codes plan_upgrade_required and payment_required rather than the original contract's subscription_required. Both semantics are equivalent; the existing codes were chosen to maintain compatibility with the public API and Portal.


TypeScript types

// ----- Output Profile -----
type ColorMode = "color" | "grayscale" | "monochrome";
type OutputFormat = "scene" | "png";

interface OutputProfile {
  formats: OutputFormat[];
  preset: string | null;
  viewport: { width: number; height: number };
  colorMode: ColorMode;
}

// ----- Scene v1 -----
type ElementType = "text" | "image" | "shape";

interface Frame { x: number; y: number; width: number; height: number; }

interface SceneElement {
  id: string;
  type: ElementType;
  frame: Frame;
  properties: Record<string, unknown>;
}

interface SceneData {
  version: 1;
  viewport: { width: number; height: number };
  background: string;
  elements: SceneElement[];
}

interface SceneV1 {
  mediaType: "application/vnd.inklet.scene+json;version=1";
  version: 1;
  data: SceneData;
}

// ----- Rendition -----
type RenditionState = "preparing" | "ready" | "failed";

interface Rendition {
  id: string;
  mediaType: "image/png";
  format: "png";
  width: number;
  height: number;
  colorMode: ColorMode;
  state: RenditionState;
  url: string | null;
  expiresAt: string | null;
  failure: Problem | null;
  updatedAt: string;
}

// ----- Targetless Presentation -----
type TargetlessPresState = "preparing" | "ready" | "failed";

interface TargetlessPresentation {
  id: string;
  displayId: null;
  contentIds: string[];
  mode: "auto" | "hardcode";
  state: TargetlessPresState;
  output: OutputProfile;
  scene: SceneV1 | null;
  renditions: Rendition[];
  image: null;
  failure: Problem | null;
  createdAt: string;
  updatedAt: string;
}

Explicitly deferred to v0.2

  • Custom Display registration
  • Device enrollment token and BYOD
  • Capability reporting
  • Quote/0 provider credentials and adapter
  • Generic Display pull protocol
  • Scene delivery / confirmation semantics
  • Dynamic device transports