npm package: @personize/sdk
Description: Official Personize SDK
Author: Personize
Latest version: 0.7.5

# Personize SDK

The official Node.js/TypeScript SDK for the Personize Product API. Authenticate with your secret key to access guidelines, smart guidelines, RAG memory, prompt execution, agents, and evaluation tools.

## Installation

```bash
npm install @personize/sdk
```

## Quick Start

```typescript
import { Personize } from '@personize/sdk';

const client = new Personize({
    secretKey: 'sk_live_...',
    // baseURL: 'http://localhost:3000' // Optional: for local development
});

// Verify API key
const testResult = await client.test();
console.log(testResult.data.organizationId);

// Read plan limits
const me = await client.me();
console.log(me.data.plan.name);
console.log(me.data.plan.limits);
```

## API Endpoints

| Method | Path | SDK Method |
| :--- | :--- | :--- |
| **Identity** | | |
| GET | `/api/v1/test` | `client.test()` |
| GET | `/api/v1/me` | `client.me()` |
| **AI** | | |
| POST | `/api/v1/ai/smart-guidelines` | `client.ai.smartGuidelines(opts)` |
| POST | `/api/v1/prompt` | `client.ai.prompt(opts)` |
| POST | `/api/v1/prompt` (stream) | `client.ai.promptStream(opts)` |
| **Memory** | | |
| POST | `/api/v1/memorize` | `client.memory.memorize(opts)` |
| POST | `/api/v1/smart-recall` | `client.memory.smartRecall(opts)` |
| POST | `/api/v1/recall` | `client.memory.recall(opts)` |
| POST | `/api/v1/search` | `client.memory.search(opts)` |
| POST | `/api/v1/batch-memorize` | `client.memory.memorizeBatch(opts)` |
| POST | `/api/v1/smart-memory-digest` | `client.memory.smartDigest(opts)` |
| POST | `/api/v1/memory/properties` | `client.memory.properties(opts)` |
| **Guidelines** | | |
| GET | `/api/v1/guidelines` | `client.guidelines.list()` |
| POST | `/api/v1/guidelines` | `client.guidelines.create(payload)` |
| GET | `/api/v1/guidelines/:id/structure` | `client.guidelines.getStructure(id)` |
| GET | `/api/v1/guidelines/:id/section` | `client.guidelines.getSection(id, opts)` |
| PATCH | `/api/v1/guidelines/:id` | `client.guidelines.update(id, payload)` |
| DELETE | `/api/v1/guidelines/:id` | `client.guidelines.delete(id)` |
| GET | `/api/v1/actions/:id/history` | `client.guidelines.history(id)` |
| **Collections** | | |
| GET | `/api/v1/collections` | `client.collections.list()` |
| POST | `/api/v1/collections` | `client.collections.create(payload)` |
| PATCH | `/api/v1/collections/:id` | `client.collections.update(id, payload)` |
| DELETE | `/api/v1/collections/:id` | `client.collections.delete(id)` |
| GET | `/api/v1/collections/:id/history` | `client.collections.history(id)` |
| **Agents** | | |
| GET | `/api/v1/agents` | `client.agents.list()` |
| GET | `/api/v1/agents/:id` | `client.agents.get(id)` |
| POST | `/api/v1/agents/:id/run` | `client.agents.run(id, opts)` |
| **Evaluation** | | |
| POST | `/api/v1/evaluate/memorization-accuracy` | `client.evaluate.memorizationAccuracy(opts)` |

All endpoints require `Authorization: Bearer sk_live_...` and count against your plan limits.

## Usage

### Guidelines

```typescript
// List all guidelines
const vars = await client.guidelines.list();

// Create a guideline
await client.guidelines.create({ name: 'ICP', value: '...', tags: ['sales'] });

// Update a guideline
await client.guidelines.update(guidelineId, { value: 'new content' });

// Get guideline structure (headings)
const structure = await client.guidelines.getStructure(guidelineId);

// Get a specific section
const section = await client.guidelines.getSection(guidelineId, { header: '## Pricing' });

// Delete a guideline
await client.guidelines.delete(guidelineId);

// Version history
const history = await client.guidelines.history(guidelineId, { limit: 5 });
```

### Smart Guidelines

```typescript
const ctx = await client.ai.smartGuidelines({
    message: 'Write a sales sequence for our top 3 ICPs',
    tags: ['sales'],
    excludeTags: ['internal'],
    sessionId: 'my-session', // for incremental context delivery
});
console.log(ctx.data.compiledContext);
```

#### Content Budget

Control how much guideline content is delivered:

```typescript
const result = await client.ai.smartGuidelines({
  message: "write a cold email",
  maxContentTokens: 5000, // deliver ~2-3 full guidelines, rest as summaries
});

// Demoted guidelines (over budget) returned with id + description for follow-up
if (result.data.budgetMetadata?.demotedGuidelines) {
  for (const g of result.data.budgetMetadata.demotedGuidelines) {
    // Fetch specific section: client.guidelines.getSection(g.id, { header: "Cold Email" })
    console.log(`${g.name}: ${g.description} — sections: ${g.sections.join(', ')}`);
  }
}
```

Default: 10,000 tokens. Guidelines too large for the budget are automatically trimmed to relevant sections. Remaining guidelines are returned as summaries with `id`, `description`, and `sections[]` for follow-up via `client.guidelines.getSection()`.

### Prompt Execution

```typescript
// Simple prompt (defaults to 'pro' tier)
const response = await client.ai.prompt({
    prompt: 'Summarize our Q4 sales strategy',
});
console.log(response.data?.metadata?.creditsCharged); // credits used

// With explicit tier
const fast = await client.ai.prompt({
    prompt: 'Quick summary of today',
    tier: 'basic',   // cheapest, fastest
});

const premium = await client.ai.prompt({
    prompt: 'Deep analysis of market trends',
    tier: 'ultra',   // highest quality model
});

// BYOK — use your own API key + model + provider (Pro/Enterprise plans only)
const byok = await client.ai.prompt({
    prompt: 'Generate a report',
    openrouterApiKey: 'sk-or-v1-...',  // your OpenRouter key
    model: 'anthropic/claude-sonnet-4-20250514',
    provider: 'openrouter',
});
// BYOK billing: flat 5 credits/call — no per-token charge, you pay your provider directly
// Without BYOK, model/provider are rejected — use tier to control quality level

// Multi-step instructions with evaluation
const result = await client.ai.prompt({
    instructions: [
        { prompt: 'Analyze the data', maxSteps: 5 },
        { prompt: 'Write a summary based on the analysis', maxSteps: 3 },
    ],
    evaluate: { criteria: 'sales', serverSide: true },
});

// Multimodal — image analysis
const analysis = await client.ai.prompt({
    prompt: 'Describe what you see in this screenshot.',
    attachments: [
        { name: 'dashboard.png', mimeType: 'image/png', data: base64EncodedImage },
    ],
});

// Multimodal — PDF extraction via URL
const extraction = await client.ai.prompt({
    prompt: 'Extract the key terms from this contract.',
    attachments: [
        { name: 'contract.pdf', mimeType: 'application/pdf', url: 'https://your-bucket.s3.amazonaws.com/contract.pdf' },
    ],
});

// Output extraction + auto-memorize
const research = await client.ai.prompt({
    instructions: [
        { prompt: 'Research the company using search tools', maxSteps: 5 },
        { prompt: 'Write a personalized outreach email', maxSteps: 3 },
    ],
    outputs: [
        { name: 'company_profile' },
        { name: 'outreach_email' },
    ],
    memorize: {
        email: 'john@acme.com',
        websiteUrl: 'acme.com',
        type: 'Company',
        captureToolResults: true,
    },
    evaluate: { criteria: 'sales', serverSide: true },
});

console.log(research.data?.outputs?.company_profile);
console.log(research.data?.evaluation?.finalScore);
```

### Streaming Prompt Execution

`promptStream()` returns an async generator that yields Server-Sent Events as they arrive. Use this for real-time UI updates, progressive output delivery, or when you need structured outputs as soon as each one finishes generating.

```typescript
import type { PromptSSEEvent } from '@personize/sdk';

// Stream with structured outputs — events arrive as each output closes
for await (const event of client.ai.promptStream({
    prompt: 'Generate a hero section and social proof for our landing page',
    outputs: [
        { name: 'hero' },
        { name: 'social_proof' },
    ],
})) {
    switch (event.type) {
        case 'text':
            // Raw LLM text chunks (everything outside <output> markers)
            process.stdout.write(event.chunk);
            break;

        case 'output':
            // A named output extracted — fired once per output as soon as </output> closes
            // event.data is already parsed (JSON object or plain string)
            console.log(`Output "${event.name}":`, event.data);
            break;

        case 'done':
            // Final event — includes all outputs, evaluation, and metadata
            console.log('All outputs:', event.outputs);
            console.log('Metadata:', event.metadata);
            break;

        case 'error':
            // Non-fatal error during streaming
            console.error('Stream error:', event.message);
            break;
    }
}
```

**SSE Event Types:**

| Event | Frequency | Description |
| :--- | :--- | :--- |
| `text` | Many per response | Plain text chunks from the LLM |
| `output` | Once per named output | Extracted output (JSON or string) — fired as soon as `</output>` closes |
| `step_complete` | Once per instruction step | Multi-step mode: step metadata |
| `done` | Once at end | Final reconciliation with all outputs + metadata |
| `error` | 0 or more | Non-fatal streaming errors |

**Options:**

| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `streamTimeout` | number | `120000` | Overall stream timeout in ms |
| `signal` | AbortSignal | — | External abort signal for cancellation |

All other `PromptOptions` parameters (`outputs`, `context`, `instructions`, `tier`, `memorize`, `evaluate`, etc.) are supported.

**Cancellation:**

```typescript
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // cancel after 5s

for await (const event of client.ai.promptStream({
    prompt: '...',
    signal: controller.signal,
})) {
    // ...
}
```

#### Entity Identifiers

All memory endpoints support multiple CRM key types for entity scoping:

| Key | Use case |
|-----|----------|
| `email` | Contact lookup |
| `websiteUrl` | Company lookup |
| `recordId` | Direct record ID |
| `customKeyName` + `customKeyValue` | Custom CRM field (e.g. `hubspot_id`) |
| `phoneNumber` | Phone-based lookup |
| `postalCode` | Location-based lookup |
| `deviceId` | Device-based lookup |
| `contentId` | Content-based lookup |

You can pass multiple keys simultaneously for better matching.

### Memory (RAG)

```typescript
// Memorize content (dual extraction: structured + free-form)
// Response includes recordId for immediate use in recall/digest calls
const memorized = await client.memory.memorize({
    content: 'Meeting notes: John prefers email contact. He is VP of Sales at Acme Corp.',
    speaker: 'Sales Rep',
    enhanced: true,
    tags: ['meetings'],
    email: 'john@example.com',
});
console.log(memorized.data?.recordId); // REC#<hex> — deterministic, use for recall/digest

// Smart recall — deep mode (default): reflection + answer generation, ~10-20s, 2 credits
const results = await client.memory.smartRecall({
    query: 'What does John prefer?',
    limit: 5,
    minScore: 0.7,
    mode: 'deep',
});

// Smart recall — fast mode: skips reflection, ~500ms, 1 credit
const fast = await client.memory.smartRecall({
    query: 'contact info for John',
    email: 'john@example.com',
    mode: 'fast',
    // min_results: 10 (default in fast mode — always returns top N even below score threshold)
});

// Custom keys — bring your own identifier for any entity type
// Use customKeyName/customKeyValue when email or websiteUrl don't apply.
// The recordId is generated deterministically from orgId + type + key — same key always resolves to the same record.
await client.memory.memorize({
    content: 'Student enrolled in Advanced AI course. GPA 3.8, Dean\'s List.',
    type: 'Student',
    customKeyName: 'studentNumber',
    customKeyValue: 'S-2024-1234',
    enhanced: true,
});

// More examples: LinkedIn profiles, web pages, code files, products...
await client.memory.memorize({ content: '...', type: 'Person',  customKeyName: 'linkedinUrl',  customKeyValue: 'https://linkedin.com/in/johndoe' });
await client.memory.memorize({ content: '...', type: 'Webpage', customKeyName: 'pageUrl',      customKeyValue: 'https://docs.example.com/api/auth' });
await client.memory.memorize({ content: '...', type: 'File',    customKeyName: 'filePath',     customKeyValue: 'src/auth/oauth.ts' });
await client.memory.memorize({ content: '...', type: 'Product', customKeyName: 'sku',          customKeyValue: 'PRD-X100-BLK' });

// Recall by the same custom key — no need to know the recordId
const student = await client.memory.smartRecall({
    query: 'What courses is this student enrolled in?',
    type: 'Student',
    customKeyName: 'studentNumber',
    customKeyValue: 'S-2024-1234',
    mode: 'fast',
});

// Direct recall — DynamoDB lookup: properties + freeform memories (no AI, type required)
const direct = await client.memory.recall({
    query: 'What do we know about Acme?',
    type: 'Contact',
    email: 'john@acme.com',
});
// direct.data.memories — structured properties from Snapshot
// direct.data.freeformMemories — AI-extracted memories from Freeform table

// Direct recall by custom key
const student = await client.memory.recall({
    query: 'Academic record',
    type: 'Student',
    customKeyName: 'studentNumber',
    customKeyValue: 'S-2024-1234',
});

// Search/filter records by property conditions — works with any type value
const found = await client.memory.search({
    groups: [{
        conditions: [{ property: 'company-name', operator: 'equals', value: 'Acme Corp' }],
    }],
    returnRecords: true,
    pageSize: 50,
});

// Search across a custom entity type
const deansList = await client.memory.search({
    type: 'Student',
    returnRecords: true,
    groups: [{
        conditions: [{ property: 'gpa', operator: 'GTE', value: 3.5 }],
    }],
});

// Access property values: records[recordId][propertyName].value (plain string)
for (const [recordId, props] of Object.entries(found.data?.records ?? {})) {
    const email = props['email']?.value;           // plain string — whatever was stored
    const company = props['company-name']?.value;
    console.log(`${recordId}: ${email} at ${company}`);
}
// Note: 'email' and 'company-name' must match the property names in your collection definition

// --- Advanced Search Patterns ---

// Key-only lookup: find a record by email without needing property conditions
const byEmail = await client.memory.search({
    email: 'sarah@acme.com',
    returnRecords: true,
});

// Key-only lookup: find a record by custom key (no groups needed)
const byStudentNumber = await client.memory.search({
    type: 'Student',
    customKeyName: 'studentNumber',
    customKeyValue: 'S-2024-1234',
    returnRecords: true,
});

// Secondary key: find Students where email is a secondary attribute (not the primary key)
// Works because memorize stores all CRM keys on every LanceDB row
const studentByEmail = await client.memory.search({
    type: 'Student',
    email: 'alice@university.edu',
    returnRecords: true,
});

// Cross-type search: find ALL records with this email across ALL entity types
// Returns Contacts, Students, Employees — whatever has this email
const crossType = await client.memory.search({
    email: 'john@acme.com',
    returnRecords: true,
});

// Property-value lookup: find records by a stored property (e.g. LinkedIn URL)
// Use this when the lookup key is a property value, not a CRM identifier
const byLinkedIn = await client.memory.search({
    type: 'Contact',
    returnRecords: true,
    groups: [{
        conditions: [{ property: 'linkedin_url', operator: 'EQ', value: 'https://linkedin.com/in/johndoe' }],
    }],
});

// Batch sync — unified call with per-property extractMemories control
// IMPORTANT: Set extractMemories: true on rich text fields (notes, transcripts, descriptions)
// to enable AI extraction and semantic search. Without it, only structured storage occurs.
await client.memory.memorizeBatch({
    source: 'Hubspot',
    mapping: {
        entityType: 'contact',
        // email: 'email' means "use the 'email' field from each row as the contact identifier"
        // The value must match the key name in your rows array (e.g., row.email)
        // Use websiteUrl instead for Company records, or recordId for direct ID matching
        email: 'email',
        properties: {
            email:     { sourceField: 'email',    collectionId: 'col_std', collectionName: 'Standard', extractMemories: false },
            full_name: { sourceField: 'fullname', collectionId: 'col_std', collectionName: 'Standard', extractMemories: false },
            notes:     { sourceField: 'notes',    collectionId: 'col_gen', collectionName: 'Generated', extractMemories: true },
        },
    },
    rows: [
        { email: 'john@acme.com', fullname: 'John Smith', notes: 'VP Sales, interested in enterprise plan' },
    ],
});

// Batch sync with custom keys — for non-Contact/Company entity types
await client.memory.memorizeBatch({
    source: 'University DB',
    mapping: {
        entityType: 'Student',
        customKeyName: 'studentNumber',   // the identifier name
        customKey: 'student_id',          // source field in rows holding the value
        properties: {
            full_name: { sourceField: 'name',   collectionId: 'col_std', collectionName: 'Standard', extractMemories: false },
            gpa:       { sourceField: 'gpa',    collectionId: 'col_std', collectionName: 'Standard', extractMemories: false },
            notes:     { sourceField: 'notes',  collectionId: 'col_gen', collectionName: 'Generated', extractMemories: true },
        },
    },
    rows: [
        { student_id: 'S-2024-1234', name: 'Alice Chen', gpa: '3.8', notes: 'Dean\'s List, AI research focus' },
        { student_id: 'S-2024-5678', name: 'Bob Park', gpa: '3.5', notes: 'Full-stack internship at Stripe' },
    ],
});

// Smart memory digest — compiled context for an entity
const digest = await client.memory.smartDigest({
    email: 'john@acme.com',
    type: 'Contact',
    token_budget: 1000,
});
console.log(digest.data?.compiledContext); // ready-to-use markdown for LLM prompts

```

### Get Record Properties

Fetch current property values for a record, joined with collection schema descriptions and the `update` flag.

```typescript
// Get specific properties with schema info
const result = await client.memory.properties({
  email: 'john@acme.com',
  propertyNames: ['Tasks', 'Lifecycle Stage'],
  nonEmpty: true,
});

for (const prop of result.data.properties) {
  console.log(`${prop.name}: ${JSON.stringify(prop.value)}`);
  console.log(`  Type: ${prop.type}, Update: ${prop.update}`);
  console.log(`  Description: ${prop.description}`);
}
```

Each property includes:
- `value` — current value
- `description` — from the collection schema, tells AI how to interpret/update
- `update` — `true` means replaceable (use `memory.update()` with `propertyValue`), `false` means append-only (use `arrayPush`)
- `type` — property type (`text`, `number`, `boolean`, `array`, `date`, `options`)
- `collectionName` — which collection this property belongs to

Options:
- `propertyNames` — filter to specific properties (recommended to save tokens)
- `includeDescriptions` — join with collection schema (default: `true`)
- `nonEmpty` — exclude null/empty values (default: `false`)
- Identifiers: `email`, `websiteUrl`, `recordId`, `customKeyName`+`customKeyValue`

### Agents

```typescript
// List agents
const agents = await client.agents.list();

// Get agent details and expected inputs
const agent = await client.agents.get(agentId);
console.log(agent.data.expectedInputs); // ['companyName', 'industry']

// Run an agent
const result = await client.agents.run(agentId, {
    inputs: { companyName: 'Acme Corp', industry: 'SaaS' },
    email: 'customer@example.com',
});
```

### Collections

Collections define the property schemas that the AI extraction pipeline uses when memorizing content.

```typescript
// List collections
const collections = await client.collections.list();

// Create a collection with properties
await client.collections.create({
    collectionName: 'Contact Properties',
    entityType: 'Contact',
    definition: 'Customer profile fields for GTM workflows',
    properties: [
        { propertyName: 'Lifecycle Stage', type: 'options', options: 'Lead, MQL, SQL, Customer', description: 'Current funnel stage', update: true },
        { propertyName: 'Meeting Notes', type: 'array', description: 'Chronological meeting log', update: false, tags: ['interaction'] },
    ],
});

// Add a single property to an existing collection (incremental -- existing properties are preserved)
await client.collections.update(collectionId, {
    properties: [
        { propertyName: 'Location', type: 'text', description: 'City or region', update: true, tags: ['identity'] },
    ],
    historyNote: 'Added Location property',
});

// Add multiple properties at once
await client.collections.update(collectionId, {
    properties: [
        { propertyName: 'Budget Range', type: 'options', options: '<50K, 50K-200K, 200K-1M, >1M', update: true },
        { propertyName: 'Decision Timeline', type: 'text', description: 'When they plan to buy', update: true },
        { propertyName: 'Objections Log', type: 'array', description: 'Sales objections raised', update: false },
    ],
    historyNote: 'Added qualification properties for lead scoring',
});

// Soft-delete a property (existing stored values are preserved)
await client.collections.update(collectionId, {
    properties: [{ propertyName: 'Old Field', systemName: 'old_field', status: 'Deleted' }],
    historyNote: 'Retired Old Field property',
});

// Delete a collection
await client.collections.delete(collectionId);

// Version history (full snapshots or compact diffs)
const history = await client.collections.history(collectionId, { mode: 'diff', limit: 10 });
```

**Property definition fields:** `propertyName` (required), `systemName` (auto-generated), `type` (`'text'` | `'number'` | `'boolean'` | `'array'` | `'date'` | `'options'`, default `'text'`), `description`, `update` (`true` = replaceable, `false` = append-only), `options`, `tags`, `status` (`'Active'` | `'Deleted'`).

See `CollectionPropertyDefinition` in `types.ts` for the full reference.

### Defaults & Limits

Default result sizes for retrieval methods. All are optional -- pass explicitly to request more or fewer.

| Method | Param | Default | Max | Notes |
|--------|-------|---------|-----|-------|
| `memory.smartRecall()` | `token_budget` | 4,000 tokens | -- | Token-budgeted, not count-based |
| `memory.recall()` | `limit` | 10 memories | 5,000 | |
| `memory.smartDigest()` | `token_budget` | 1,000 tokens | -- | Also accepts `maxMemories` (default 20) |
| `memory.search()` / `memory.filterByProperty()` | `pageSize` | 50 records | 200 | |
| `memory.similar()` | `topK` | 25 records | 5,000 | |
| `memory.segment()` | `maxPerTier` | 50 per tier | -- | No hard cap |
| `memory.memorize()` (batch) | items | -- | 100 | Optimal: 10-50 per call |

### Evaluation

```typescript
// Run memorization accuracy evaluation against a collection
const evaluation = await client.evaluate.memorizationAccuracy({
    collectionId: 'col_123',
    input: 'John Smith is VP of Sales at Acme Corp, based in NYC.',
    skipStorage: true,
});
console.log(evaluation.data.summary.propertiesOptimized);
```

## Tiers & Pricing

### Generate Tiers (Prompt)

| Tier | Input Credits/1K tokens | Output Credits/1K tokens | Best For |
| :--- | :--- | :--- | :--- |
| `basic` | 1.0 | 2.0 | High-volume, simple tasks |
| `pro` | 1.0 | 3.0 | Balanced quality & cost (default) |
| `ultra` | 1.0 | 5.0 | Complex reasoning, highest quality |

### Memorize Tiers

| Tier | Credits/1K tokens | Best For |
| :--- | :--- | :--- |
| `basic` | 1 | Bulk imports, simple data |
| `pro` | 3 | General use (default) |
| `pro_fast` | 5 | Pro quality, lower latency |
| `ultra` | 12 | Maximum extraction quality |

### Retrieval (Recall & Smart Guidelines)

| Operation | Mode | Credits/Call |
| :--- | :--- | :--- |
| Smart Recall | `mode: 'fast'` | 1 |
| Smart Recall | `mode: 'deep'` | 2 |
| Smart Guidelines | `mode: 'fast'` | 1 |
| Smart Guidelines | `mode: 'deep'` | 1 |

Smart Recall cost depends on mode: fast = 1 credit, deep = 2 credits. All other read operations (recall, smart guidelines, smart context) charge a flat **1 credit per call**.

1 credit = $0.01.

### Direct Providers

### BYOK (Bring Your Own Key)

Pro and Enterprise plans can pass their own API key. Billing switches to a flat **5 credits per call** (no per-token charge — you pay your provider directly).

When using BYOK, you **must** provide both `model` and `provider`. Without BYOK, `model` and `provider` are rejected — use `tier` to control quality level.

```typescript
// BYOK: must specify all three
await client.ai.prompt({
    prompt: '...',
    openrouterApiKey: 'sk-or-v1-...',
    model: 'anthropic/claude-sonnet-4-20250514',
    provider: 'openrouter',
});

// Without BYOK: use tier (model/provider auto-selected)
await client.ai.prompt({
    prompt: '...',
    tier: 'pro',  // basic, pro (default), ultra
});
```

**Error cases:**
- `model`/`provider` without `openrouterApiKey` → `400 BYOK required`
- `openrouterApiKey` without `model`/`provider` → `400 model and provider required`
- BYOK on a plan that doesn't allow it → `403 byok_not_allowed`

Response metadata includes `byok: true` and `creditsCharged` reflecting the flat 5-credit charge.

## Smart Recall Modes

The `mode` parameter controls smart recall behavior:

| Mode | Latency | Credits | Description |
| :--- | :--- | :--- | :--- |
| `"fast"` | ~500ms | 1 | Skips reflection and answer generation. Returns raw vector results with guaranteed minimum count. |
| `"deep"` | ~10-20s | 2 | Enables reflection + answer generation for higher-quality, synthesized responses. |

Default is `"deep"`. The `mode` parameter takes precedence over the individual `enable_reflection`, `generate_answer`, and `fast_mode` flags. If `mode` is set, those flags are ignored.

#### Recency Bias

Use `prefer_recent` to boost recent memories with exponential decay:

```typescript
const result = await client.memory.smartRecall({
  query: "what changed on this contact recently?",
  email: "john@acme.com",
  mode: "deep",
  prefer_recent: true,
  recency_half_life_days: 30, // aggressive: 30-day half-life
});
```

Default half-life is 90 days. A memory 90 days old scores ~37% of its original relevance. Set to 7-30 for "what happened this week/month" queries.

## Best Practices: Query Crafting for smartRecall

The `smartRecall` endpoint uses vector similarity search. Query quality directly impacts result relevance. When building AI pipelines that call `smartRecall`, the **AI agent is responsible for crafting embedding-friendly queries**.

**Do:**
- Use specific, descriptive queries that match the language of stored data
- Include entity names, property names, or domain-specific terms
- Example: `"John Smith role title company background"` instead of `"Tell me about this contact"`

**Don't:**
- Use vague meta-queries like `"What do we know?"` or `"Tell me everything"`
- Use task-oriented queries like `"open tasks pending action items"` when only profile data was memorized

**Example — AI pipeline pattern:**
```typescript
// BAD: vague query → low similarity scores → few or no results
const bad = await client.memory.smartRecall({ query: 'Tell me about this contact', email });

// GOOD: specific query targeting stored data types
const good = await client.memory.smartRecall({
    query: `${contactName} role company background interests preferences`,
    email,
    mode: 'fast',
});
```

**Guaranteed minimum results:** In `mode: 'fast'`, `smartRecall` guarantees at least 10 results (configurable via `min_results`) even when scores fall below the threshold. This ensures your AI workflow always has context to reason about — it can then decide whether the data is sufficient or not.

## Configuration

| Option | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `secretKey` | string | Yes | Your secret key (`sk_live_...`). |
| `baseURL` | string | No | Custom API endpoint (default: `https://agent.personize.ai`). |
| `timeout` | number | No | Request timeout in ms (default: `30000`). |
| `maxRetries` | number | No | Max retry attempts for 429/5xx errors (default: `3`). |
| `retryDelay` | number | No | Base delay in ms for exponential backoff (default: `1000`). |

## Test Key (Local Development)

```typescript
const client = new Personize({
    secretKey: 'sk_live_TEST_KEY_00000000000000000000000000000000000000000000000000000000',
    baseURL: 'http://localhost:3000',
});
```

## Migration from 0.6.2

**New in 0.6.3:**

| Feature | Details |
| :--- | :--- |
| `evaluationCriteria` on `PromptOptions` | Compatibility alias for `evaluate: { criteria, serverSide: true }` |
| `message` on `RecallOptions` / `SmartRecallOptions` | Compatibility alias for `query` |
| Legacy `memory.recall()` routing | If you call `recall()` with legacy advanced-recall-style inputs (`message`, `limit`, omitted `type`, collection-name scoping), the SDK routes to `/smart-recall` automatically |
| Shorthand semantic `memory.search()` | `search({ query, limit, collectionName })` is accepted and routed to `/smart-recall` when no filter groups are provided |
| `collectionName` / `collectionNames` on `memorize()` | Collection names are resolved client-side into `collectionIds` |
| Legacy collection payload aliases | `collections.create()` now accepts `name`, `slug`, `description`, array `options`, and `updateSemantics` |
| `records` shorthand on `memorizeBatch()` | Accepts record-style input and normalizes it client-side into `memorize()` + `/batch-memorize` calls |

See [SDK_0_6_3_COMPATIBILITY_CHANGES.md](SDK_0_6_3_COMPATIBILITY_CHANGES.md) for the full design notes and tradeoffs.

## Migration from 0.5.x

**New in 0.6.0:**

| Feature | Details |
| :--- | :--- |
| `tier` on `PromptOptions` | Select generate tier: `basic`, `pro` (default), `ultra` |
| `tier` on `MemorizeOptions` / `BatchMemorizeOptions` | Select memorize tier: `basic`, `pro`, `pro_fast`, `ultra` |
| `openrouterApiKey` on `PromptOptions` | BYOK — use your own API key (Pro/Enterprise plans). Requires `model` + `provider`. |
| `model` / `provider` on `PromptOptions` | Custom model/provider selection. **Requires BYOK** (`openrouterApiKey`). Without BYOK, use `tier`. |
| `creditsCharged` in response metadata | Credits consumed by the request |
| SmartGuidelines `mode: 'full'` → `mode: 'deep'` | Renamed for consistency. `'full'` still accepted for backward compatibility. |

## Migration from 0.3.x

**Breaking changes in 0.4.0:**

| 0.3.x | 0.4.0 | Notes |
| :--- | :--- | :--- |
| `client.memory.recall(opts)` | `client.memory.smartRecall(opts)` | Advanced recall with reflection (route changed to `/smart-recall`) |
| `client.memory.export(opts)` | `client.memory.search(opts)` | Filter/search records (route changed to `/search`) |
| `MemorizeProOptions` | `MemorizeOptions` | Type renamed (old name still available as deprecated alias) |
| `RecallProOptions` | `SmartRecallOptions` | Type renamed (old name still available as deprecated alias) |
| `ExportOptions` / `ExportResponse` | `SearchOptions` / `SearchResponse` | Types renamed (old names still available as deprecated aliases) |

**New in 0.4.0:**
- `client.test()` — Verify API key validity
- `client.agents.get(id)` — Get agent details and expected inputs
- `client.memory.recall(opts)` — Direct DynamoDB lookup: properties + freeform memories (no AI)
- `client.memory.search(opts)` — Search/filter records with `property`-based conditions
- `client.evaluate.memorizationAccuracy(opts)` — Three-phase memorization evaluation
- `attachments` on `PromptOptions` — Multimodal support (images, PDFs, documents)
- `sessionId` on `SmartGuidelinesOptions` — Progressive context delivery across multi-step workflows
- New fields on `MemorizeOptions`: `collectionIds`, `skipStorage`, `skipDualWrite`, `skipPropertySelection`
- New fields on `SmartRecallOptions`: `max_reflection_rounds`, `filters`

## Skills

AI-assistant skills for Claude Code, Codex, and Cursor are available separately:

```bash
npx skills add personizeai/personize-skills --all
```

See [personizeai/personize-skills](https://github.com/personizeai/personize-skills) for the full catalog.