---
title: Widget SDK reference
slug: content-recommendations/docs/personalisation-sdk
docTags: 
createdAt: 2026-08-05T19:40:07.989Z
---

The `ip.js` SDK is designed to be flexible, enabling both static widget placements and dynamic implementations. The script scans your page for special `<script>` tags that serve as both configuration and templates for recommendation widgets.

## Configuration methods

You can configure widgets using `data-*` attributes directly on the `<script>` tag. This is the simplest and most common method.

**Example**

See the following example:

```html
<script
    type="text/x-mustache"
    class="idio-recommendations"
    data-api-key="YOUR_DELIVERY_KEY_HERE"
    data-rpp="5"
    data-section="Case Studies"
    data-fallback="Popular Content"
></script>
```

## Configuration options

The following `data-*` attributes are available for widget configuration.

| Attribute              | Required | Description                                                                                                                                                                    |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `data-api-key`         | Yes      | The unique API key for this widget delivery.                                                                                                                                   |
| `data-rpp`             | No       | The number of content items to fetch (Results Per Page). . Overrides the server-side setting.                                                                                  |
| `data-page`            | No       | The page number of results to fetch, for pagination. Defaults to `1`.                                                                                                          |
| `data-filter`          | No       | A Lucene query used for personalized recommendations. If `data-section` is also present, `data-filter` takes precedence.                                                       |
| `data-section`         | No       | This parameter is ignored if `data-filter` is provided. If you are using a filter you should specify the section as part of the query. Only include content from this section. |
| `data-fallback`        | No       | A fallback section name or strategy, for example, `popular` or `recent`, used if personalization is not possible.                                                              |
| `data-fallback-filter` | No       | A Lucene query used as a fallback. Takes precedence over `data-fallback`.                                                                                                      |

## Templating

Widgets are rendered using the [Mustache](https://mustache.github.io/) templating engine. The `ip.js` SDK passes an object to your template, with the primary property `content`, which is an array of recommended items.

**Available Template Variables**

Within a `{{#content}}...{{/content}}` block, you have access to the following variables for each item:

- `{{title}}` –  The title of the content
- `{{link_url}}` – The destination URL
- `{{main_image_url}}` – The URL for the main image
- `{{abstract}}` – A short summary of the content
- `{{topics}}` – A list of topics associated with the content
- `{{source}}` – The source of the content item
- `{{author}}` – The author of the content item

:::hint{type="warning"}
**Important**
To prevent HTML injection, all variables rendered with `{{variable}}` are HTML-escaped. If you need to render raw HTML, use the triple-mustache – `{{{variable}}}`. Use this with caution and only with trusted content.
:::

### Advanced usage with callbacks

For more dynamic control, define a global `window._ipc` object **before** the `ip.js` script is loaded. This lets you define global settings and callbacks.

**Global callbacks example**

See the following example:

```html
<script type="text/javascript">
var _ipc = {
  complete: function(tag, statusCode) {
    console.log('Success! Widget has been rendered.', tag);
  },
  error: function(tag, statusCode) {
    console.log('There was an error rendering the widget:', statusCode);
  }
};
</script>
```

Data attributes on an individual widget always override any settings defined in the global `_ipc` object.

## Advanced callbacks

For complete control over widget rendering, use the `finish` callback. This callback is invoked when the API response is received, letting you implement custom rendering logic, A/B testing, or conditional widget display.

### The `finish` callback

The `finish` callback can be defined either in the global `_ipc` object or in individual widget configurations. It receives four parameters:

```javascript
finish: function(apiResponse, statusCode, trackLinks, render) {
    // Your custom logic here
}
```

**Parameters:**

| Parameter     | Type     | Description                                                                          |
| ------------- | -------- | ------------------------------------------------------------------------------------ |
| `apiResponse` | Object   | The complete API response containing recommendations and metadata.                   |
| `statusCode`  | Number   | The HTTP status code of the API response like `200`, `204`, `404`.                   |
| `trackLinks`  | Function | A helper function to wrap existing links with click tracking.                        |
| `render`      | Function | A helper function to render a Mustache template and mark the recommendation as seen. |

### API response structure

The `apiResponse` object contains the following key properties:

`apiResponse.content` (Array)
An array of recommended content items. Each item contains:

- `title` – Content title
- `link_url` – Destination URL
- `main_image_url` – Main image URL
- `abstract` – Content summary
- `topics` – Associated topics
- `source` – Content source
- `author` – Content author

`apiResponse.group` (String)
The A/B test group assignment for the visitor. Use this to implement different behavior for test vs. control groups:

- `'control'` – User is in the control group (no personalization)
- `'test'` – User is in the test group (personalized content)

`apiResponse.model` (String)
Information about the personalization model used for this visitor. Indicates whether recommendations are based on a user interest profile or fallback logic.

`apiResponse.recommendation_id` (String)
A unique identifier for this recommendation decision. This ID is used internally for tracking impressions and clicks.

### Callback helper functions

`trackLinks(arrayOfLinks)`

Wraps an array of `<a>` elements with click tracking URLs. This is useful when you want to track clicks on existing content rather than rendered recommendations.

```javascript
finish: function(apiResponse, statusCode, trackLinks, render) {
    var existingLinks = document.querySelectorAll('.content-area a');
    trackLinks(existingLinks);
}
```

`render(apiResponse, statusCode, templateString)`

Renders a Mustache template with the API response data and marks the recommendation as seen in the analytics system. Returns the rendered HTML string.

```javascript
finish: function(apiResponse, statusCode, trackLinks, render) {
    var template = '<div>{{#content}}<a href="{{link_url}}">{{title}}</a>{{/content}}</div>';
    var html = render(apiResponse, statusCode, template);
    document.getElementById('widget-container').innerHTML = html;
}
```

:::hint{type="warning"}
**Important**
When using the `finish` callback, recommendations are created with a `pending` status. The recommendation is marked as visible and counted in analytics only when you call the `render()` function. If you do not call `render()`, the recommendation does not display in dashboard metrics.
:::

## Use cases and examples

### A/B testing with control groups

Use the `apiResponse.group` field to display different content to test and control groups:

```javascript
var _ipc = [{
    api_key: 'YOUR_DELIVERY_KEY',
    section: 'Blog Posts',
    finish: function(apiResponse, statusCode, trackLinks, render) {
        if (apiResponse.group === 'control') {
            // Control group: Track existing static content
            var staticLinks = document.querySelectorAll('#static-content a');
            trackLinks(staticLinks);
            console.log('User in control group - tracking static content');
        } else {
            // Test group: Render personalized widget
            var template = document.getElementById('widget-template').innerHTML;
            var html = render(apiResponse, statusCode, template);
            document.getElementById('personalized-widget').innerHTML = html;
            console.log('User in test group - showing personalized content');
        }
    }
}];
```

### Conditional rendering based on content availability

Render the widget only if sufficient recommendations are available:

```javascript
var _ipc = [{
    api_key: 'YOUR_DELIVERY_KEY',
    rpp: 5,
    finish: function(apiResponse, statusCode, trackLinks, render) {
        if (statusCode === 200 && apiResponse.content && apiResponse.content.length >= 3) {
            // Enough content available, render the widget
            var template = '{{#content}}<div class="item">{{title}}</div>{{/content}}';
            var html = render(apiResponse, statusCode, template);
            document.getElementById('recommendations').innerHTML = html;
        } else {
            // Not enough content, hide the widget container
            document.getElementById('recommendations').style.display = 'none';
        }
    }
}];
```

### Custom template selection based on personalization

Use different templates depending on whether content is personalized:

```javascript
var _ipc = [{
    api_key: 'YOUR_DELIVERY_KEY',
    finish: function(apiResponse, statusCode, trackLinks, render) {
        var template;

        if (apiResponse.model && apiResponse.model.includes('personalized')) {
            // Use premium template for personalized content
            template = document.getElementById('personalized-template').innerHTML;
        } else {
            // Use basic template for non-personalized content
            template = document.getElementById('basic-template').innerHTML;
        }

        var html = render(apiResponse, statusCode, template);
        document.getElementById('widget-area').innerHTML = html;
    }
}];
```

### Programmatic widget loading

Dynamically load widgets based on user interaction:

```javascript
function loadRecommendations(section, containerId) {
    var config = {
        api_key: 'YOUR_DELIVERY_KEY',
        section: section,
        finish: function(apiResponse, statusCode, trackLinks, render) {
            if (statusCode === 200) {
                var template = '{{#content}}<a href="{{link_url}}">{{title}}</a>{{/content}}';
                var html = render(apiResponse, statusCode, template);
                document.getElementById(containerId).innerHTML = html;
            }
        }
    };

    // Use the idio.load() API to load recommendations dynamically
    idio.load([config]);
}

// Call when user clicks a button
document.getElementById('load-more').addEventListener('click', function() {
    loadRecommendations('Related Articles', 'dynamic-widget-1');
});
```
