> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reelevant.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Core SDK

> Use the @rlvt/web-sdk core module with any Node.js server, edge runtime, or custom framework

## Installation

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @rlvt/web-sdk
```

The core SDK has zero dependencies and works with any runtime that supports the Web Fetch API (Node.js 18+, Bun, Deno, Cloudflare Workers).

## Client setup

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { ReelevantClient } from '@rlvt/web-sdk'

const rlvt = new ReelevantClient({
  // timeout: 5000,        // default: 5000ms
  // fallback: 'empty',    // default: 'empty' — returns empty result on error
})
```

No configuration is required — the SDK defaults to the production runner.

### Configuration options

| Option      | Type                             | Default                 | Description                    |
| ----------- | -------------------------------- | ----------------------- | ------------------------------ |
| `runnerUrl` | `string`                         | `https://reelevant.run` | Runner endpoint URL            |
| `timeout`   | `number`                         | `5000`                  | Global timeout in milliseconds |
| `fallback`  | `'empty' \| 'error' \| Function` | `'empty'`               | What happens when a call fails |

## Fetching personalised content

### Single workflow

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { extractUserId } from '@rlvt/web-sdk'

// Extract user identity from cookies
const userId = extractUserId(req.headers.cookie ?? '')

const result = await rlvt.run({
  workflowId: 'your-workflow-id',
  entrypoint: '43a490a0',
  userId,
  userAgent: req.headers['user-agent'],
  ip: req.headers['x-forwarded-for']?.split(',')[0],
  referer: req.url,
})
```

### Multiple workflows in parallel

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const [hero, sidebar, footer] = await rlvt.runAll([
  { workflowId: 'wf-hero', entrypoint: '43a490a0', userId },
  { workflowId: 'wf-sidebar', entrypoint: 'b7e21f3c', userId },
  { workflowId: 'wf-footer', entrypoint: 'd9c84e1a', userId },
])
```

### Run options

| Option       | Type                      | Description                                               |
| ------------ | ------------------------- | --------------------------------------------------------- |
| `workflowId` | `string`                  | Workflow ID from the Reelevant dashboard                  |
| `entrypoint` | `string`                  | Entrypoint shortId (8-char alphanumeric, e.g. `43a490a0`) |
| `userId`     | `string?`                 | Visitor identity from cookies                             |
| `params`     | `Record<string, string>?` | URL parameters forwarded to the runner                    |
| `locale`     | `string?`                 | Locale for content resolution                             |
| `userAgent`  | `string?`                 | Forwarded for device detection                            |
| `ip`         | `string?`                 | Forwarded for geolocation                                 |
| `referer`    | `string?`                 | Page URL                                                  |
| `timeout`    | `number?`                 | Per-call timeout override in ms                           |

## Handling the response

`run()` returns a `RunResult` with a discriminated union `body`:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const result = await rlvt.run({ workflowId: '...', entrypoint: '...' })

switch (result.body.type) {
  case 'html':
    // result.body.content is a string of HTML
    res.send(result.body.content)
    break

  case 'json':
    // result.body.content is a parsed JSON object
    res.json(result.body.content)
    break

  case 'image':
    // result.body.content is an ArrayBuffer
    res.setHeader('Content-Type', 'image/png')
    res.send(Buffer.from(result.body.content))
    break

  case 'empty':
    // No content returned — show your default
    res.send('<div>Default content</div>')
    break
}
```

### RunResult fields

| Field            | Type                      | Description                                             |
| ---------------- | ------------------------- | ------------------------------------------------------- |
| `status`         | `number`                  | HTTP status code (0 for fallback)                       |
| `source`         | `'runner' \| 'fallback'`  | Where the result came from                              |
| `body`           | `RunContent`              | Typed content (see above)                               |
| `metadata`       | `Record<string, unknown>` | Metadata from the output node                           |
| `properties`     | `Record<string, unknown>` | Output properties                                       |
| `runId`          | `string \| null`          | Workflow run ID for tracking                            |
| `executionPath`  | `string[]`                | Branch IDs taken during execution                       |
| `redirectionUrl` | `string`                  | Pre-built click-through URL (runner with `mode=click`)  |
| `trackClick`     | `() => Promise<void>`     | Fire-and-forget callback to track the click server-side |

## Click tracking

<Warning>
  **Click tracking must always be set up after display.** The flow is: fetch content → display it to the user → track clicks when they interact. Every content display should have a corresponding click tracking mechanism, otherwise you lose attribution data.
</Warning>

Every `RunResult` includes a `redirectionUrl` and a `trackClick()` callback. This ensures click tracking is always recorded — devs must use one of the two patterns below.

### Option 1: Redirect link (recommended for CTAs)

Use `redirectionUrl` as the `href` on your call-to-action links. The browser handles everything — the runner tracks the click and 302-redirects to the final destination.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const result = await rlvt.run({ workflowId: 'wf-hero', entrypoint: '43a490a0', userId })

// In your template — always pair display with click tracking
// <a href="${result.redirectionUrl}">Shop now</a>
```

### Option 2: Server-side fire-and-forget

Call `result.trackClick()` when you control navigation and want to track the click in the background. It calls the runner click endpoint with `redirect: manual` and swallows all errors — it never throws.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const result = await rlvt.run({ workflowId: 'wf-hero', entrypoint: '43a490a0', userId })

// User clicked a CTA (must be called after display) — track it server-side
await result.trackClick()
```

No arguments needed — the callback is pre-bound to the result's `redirectionUrl`.

## Identity helpers

### `extractUserId(cookies)`

Extracts the Reelevant user ID from cookies. Priority: `rlvt_clientId` > `rlvt_tmpId`.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { extractUserId } from '@rlvt/web-sdk'

// From a cookie header string
const userId = extractUserId(req.headers.cookie ?? '')

// From a parsed cookies object
const userId = extractUserId(req.cookies)
```

### `generateTmpId()`

Generates a new anonymous temporary ID, matching the format used by the client-side tracker.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { generateTmpId } from '@rlvt/web-sdk'

const tmpId = generateTmpId()
// Set as rlvt_tmpId cookie on the response
```

## Fallback strategies

Configure how the SDK handles timeouts and errors:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
// 'empty' (default) — returns an empty result, your page renders normally
const rlvt = new ReelevantClient({ fallback: 'empty' })

// 'error' — throws the original error, you handle it in your catch block
const rlvt = new ReelevantClient({ fallback: 'error' })

// Custom function — return your own fallback content
const rlvt = new ReelevantClient({
  fallback: (options, error) => ({
    status: 0,
    source: 'fallback',
    body: { type: 'html', content: '<div>Default banner</div>' },
    metadata: {},
    properties: {},
    runId: null,
    executionPath: [],
  }),
})
```

## Request flow

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant Client as Browser
    participant Server as Your Server
    participant SDK as @rlvt/web-sdk
    participant Runner as Reelevant Runner

    Client->>Server: GET /page
    Server->>SDK: extractUserId(cookies)
    SDK-->>Server: userId (or undefined)
    Server->>SDK: rlvt.run({ workflowId, entrypoint, userId })
    SDK->>Runner: GET /{workflowId}/{entrypoint}?rlvt-u={userId}
    Runner-->>SDK: HTML / JSON / Image + headers
    SDK-->>Server: RunResult { body, metadata, runId }
    Server->>Server: Inject content into page template
    Server-->>Client: Full HTML with personalised content
```

## Express example

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import express from 'express'
import { ReelevantClient, extractUserId, generateTmpId } from '@rlvt/web-sdk'

const app = express()
const rlvt = new ReelevantClient()

app.get('/page', async (req, res) => {
  // Ensure identity cookie
  let userId = extractUserId(req.headers.cookie ?? '')
  if (!userId) {
    const tmpId = generateTmpId()
    res.cookie('rlvt_tmpId', tmpId, { maxAge: 365 * 24 * 60 * 60 * 1000 })
    userId = tmpId
  }

  const result = await rlvt.run({
    workflowId: 'wf-hero',
    entrypoint: '43a490a0',
    userId,
    userAgent: req.headers['user-agent'],
    ip: req.ip,
    referer: req.originalUrl,
  })

  const heroHtml = result.body.type === 'html' ? result.body.content : ''

  res.send(`
    <html>
      <body>
        <div data-rlvt-ssr="true">${heroHtml}</div>
      </body>
    </html>
  `)
})
```

## Constants

The SDK exports commonly used constants:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  DEFAULT_RUNNER_URL,   // 'https://reelevant.run'
  DEFAULT_TIMEOUT,      // 5000
  COOKIE_CLIENT_ID,     // 'rlvt_clientId'
  COOKIE_TMP_ID,        // 'rlvt_tmpId'
} from '@rlvt/web-sdk'
```
