---
title: "Create a widget"
description: "Create a widget that shows an assistant to anonymous visitors, and get its install snippets. Creating is free; visitors' answers are charged to the workspace."
canonical: "https://lookio.app/docs/api/widgets/create-widget"
updated: "2026-10-25"
---

# Create a widget

Requires `assistants:write`.

Creating a widget costs no credits. Each visitor question that reaches the model spends the widget's `mode` price (`flash` by default, 30 credits), whether or not Lookio finds an answer. Say so to whoever sets it up, and set `limits` to cap the spending.

- **Allowed domains.** List the websites that show the widget in `allowed_domains`, for example `docs.acme.com`. `acme.com` also covers `www.acme.com`, and `*.acme.com` covers every subdomain. The browser refuses the bubble and the embed on any site not listed. With no domain, nothing shows on any site; only the hosted page works, once you turn it on.
- **Formats.** Each format is on or off with a setting: `settings.bubble` (on by default), `settings.embed` (off) and `settings.hosted_page` (off). A new widget starts with the chat bubble only. A format that is off does not exist: the loader script draws no launcher, and the in-page frame and the hosted page answer 404. `snippets.iframe` belongs to the embed format.
- **Limits.** By default a visitor may ask 20 questions per hour and 60 per day, and the widget may spend 3,000 credits per calendar month (UTC). Set `limits.monthly_credits` to `null` for no ceiling beyond the workspace's balance. See [Access and limits](/docs/widget/access-and-limits).
- **Sources.** `settings.sources` is `numbered` (the default), `numbered_no_links`, `inline_links` or `none`. See [Appearance](/docs/widget/appearance#sources).
- **Snippets.** The response holds the code to install it: `snippets.bubble` before `</body>` for a launcher, `snippets.embed` where the chat should sit, `snippets.hosted` for a full page and `snippets.iframe` where scripts are refused. See [Install a widget](/docs/widget/install).

The widget is live as soon as it exists. Send an `Idempotency-Key` header so a retried request does not create two widgets.

```http
POST https://api.lookio.app/v1/widgets
```

## Authorizations

- `Authorization` (bearer, in header, required)

## Header parameters

- `Idempotency-Key` (`string`, length: `1–255 chars`): A unique key for this request, such as a UUID, 1 to 255 visible ASCII characters. Send the same key when you retry: the first response is returned (with `Idempotent-Replayed: true`) instead of running the request again, for 24 hours, per credential. The same key on a different request is a 409 `conflict`. A streamed answer is not recorded.

## Body

`application/json`, required

- `assistant_id` (`string`, required, pattern: `^ast_[abcdefghjkmnpqrstuvwxyz23456789]{12}$`): The assistant the widget speaks for. A technical identity limited to some assistants can only pick one of them.
- `name` (`string`, length: `1–120 chars`): 1 to 120 characters. Defaults to the assistant's name followed by `widget`.
- `mode` (`enum<string>`, one of: `eco, flash, deep`): The query mode every answer of this widget uses, so what it costs per answer (Eco 10, Flash 30 credits (the default), Deep 80). Default flash.
- `status` (`enum<string>`, one of: `live, paused`): `live` answers visitors; `paused` shows nothing and answers nothing, until set live again.
- `allowed_domains` (`string[]`, items: `≤ 50 items`): The websites allowed to show the widget, as hosts (`docs.acme.com`) or `*.acme.com` for all subdomains (which also covers `acme.com`). Scheme, path and port are removed. The browser refuses to show the bubble and the embed on any other site, so a widget with no domain shows nowhere (the hosted page still works). Replaces the previous list.
- `settings` (`object`): Appearance, words and behaviour. Every field is optional and has a default.
- `settings.theme` (`enum<string>`, one of: `light, dark, auto`): `light`, `dark`, or `auto` to follow the visitor's system.
- `settings.accent` (`string`, pattern: `^#[0-9a-fA-F]{6}$`): The brand colour of the launcher and the buttons, as `#RRGGBB`.
- `settings.radius` (`enum<string>`, one of: `none, small, medium, large`): How round the corners are: `none`, `small`, `medium` or `large`.
- `settings.bubbles` (`enum<string>`, one of: `corners, round`): The shape of the visitor's message bubbles and the suggestion chips: `corners` (default) follows `radius`, `round` makes them fully round whatever the corners are. Omit it to leave it as it is.
- `settings.position` (`enum<string>`, one of: `bottom-right, bottom-left`): Which corner of the page the bubble sits in: `bottom-right` or `bottom-left`.
- `settings.launcher` (`enum<string>`, one of: `bubble, pill, bar`): `bubble`: a round button in a corner. `pill`: a button with `launcher_label`. `bar`: a bar centered at the bottom of the page, showing the `placeholder`, that opens the chat in the middle of the screen (`position` does not apply).
- `settings.launcher_label` (`string`, length: `≤ 40 chars`): The text of the pill launcher, up to 40 characters.
- `settings.launcher_icon` (`enum<string>`, one of: `chat, message, help, sparkles, bot, search, book, bulb, bolt, heart, star, support`): The launcher's icon: `chat` (default), `message`, `help`, `sparkles`, `bot`, `search`, `book`, `bulb`, `bolt`, `heart`, `star` or `support`.
- `settings.launcher_size` (`enum<string>`, one of: `small, medium, large`): The launcher's size, whatever its style: `small`, `medium` (default) or `large`.
- `settings.bar_icon` (`boolean`): The `bar` launcher: show the icon at its start (default). Off: a plain field.
- `settings.bar_translucent` (`boolean`): The `bar` launcher: see-through, with the page blurred behind it. Off (default): a solid bar.
- `settings.header_icon` (`enum<string>`, one of: `chat, message, help, sparkles, bot, search, book, bulb, bolt, heart, star, support, none`): The avatar in the chat's header, apart from the launcher's icon: `sparkles` (default), `chat`, `message`, `help`, `bot`, `search`, `book`, `bulb`, `bolt`, `heart`, `star` or `support`; `none` shows no avatar at all.
- `settings.title` (`string`, length: `≤ 60 chars`): The chat's title, up to 60 characters. Empty means the assistant's name.
- `settings.subtitle` (`string`, length: `≤ 80 chars`): A line under the chat's title, up to 80 characters, like `Available 24/7`. Empty (default): none, and the title sits centered alone. Omit it to leave it as it is.
- `settings.greeting` (`string`, length: `≤ 500 chars`): The first message the visitor sees, up to 500 characters.
- `settings.placeholder` (`string`, length: `≤ 80 chars`): The hint in the question box, up to 80 characters.
- `settings.suggestions` (`string[]`, items: `≤ 4 items`): Up to 4 example questions (120 characters each) shown before the first question.
- `settings.disclaimer` (`string`, length: `≤ 200 chars`): The small print under the chat, up to 200 characters. Empty hides it.
- `settings.language` (`enum<string>`, one of: `auto, en, fr, de, es, it, pt`): The language of the widget's own words (buttons, notices): `auto` follows the visitor, or `en`, `fr`, `de`, `es`, `it`, `pt`. Answers always follow the visitor's language.
- `settings.sources` (`enum<string>`, one of: `numbered, numbered_no_links, inline_links, none`): How answers show their sources: `numbered` ([1] markers and a source list with links to web pages), `numbered_no_links` (the same without any address), `inline_links` (the answer links its own words to the source pages, no list; describe the style in the assistant's context) or `none`.
- `settings.memory` (`boolean`): Follow-up questions see the conversation so far.
- `settings.visitor_history` (`boolean`): Visitors find their past chats when they come back on the same browser (a random id kept in the chat frame's own storage). Off: nothing is kept in the browser and every visit starts fresh.
- `settings.feedback` (`boolean`): Thumbs up and down under each answer, with an optional word on what was wrong after a thumbs down. Read them in the workspace's logs: `GET /v1/logs/conversations?widget_id=…&rating=down` lists the conversations where visitors were not satisfied.
- `settings.branding` (`boolean`): Show "Powered by Lookio". Only paid plans may turn it off.
- `settings.bubble` (`boolean`): The chat bubble format: a launcher in a corner of the page that opens the chat. On by default. Off: the loader draws nothing.
- `settings.embed` (`boolean`): The in-page format: the chat inside a container of your page (`snippets.embed`, or `snippets.iframe`). Off by default; off, the frame answers 404.
- `settings.hosted_page` (`boolean`): The hosted page format: a full-page chat at `snippets.hosted`, to link to. Off by default; off, the page answers 404.
- `limits` (`object`): Spending and abuse limits. Every field is optional and has a default.
- `limits.monthly_credits_enabled` (`boolean`)
- `limits.per_visitor_hour` (`integer`, range: `1–500`): Questions one visitor may ask per hour, 1 to 500.
- `limits.per_visitor_day` (`integer`, range: `1–2000`): Questions one visitor may ask per day, 1 to 2,000, and not fewer than the hourly limit.
- `limits.monthly_credits` (`integer | null`): Credits the widget may spend per calendar month (UTC), 10 to 10,000,000. Null: no ceiling beyond the workspace's balance. When it is reached the widget stops answering until the month turns.

## Response

**201**: Create a widget

- `data` (`object`, required)
- `data.id` (`string`, required, pattern: `^wdg_[abcdefghjkmnpqrstuvwxyz23456789]{12}$`)
- `data.name` (`string`, required)
- `data.assistant_id` (`string`, required)
- `data.assistant_name` (`string,null`, required): Null when the assistant was deleted: the widget answers nothing until you point it at another.
- `data.mode` (`enum<string>`, required, one of: `eco, flash, deep`)
- `data.status` (`enum<string>`, required, one of: `live, paused`): `live` answers visitors; `paused` shows nothing and answers nothing, until set live again.
- `data.allowed_domains` (`string[]`, required)
- `data.settings` (`object`, required)
- `data.settings.theme` (`enum<string>`, required, default: `auto`, one of: `light, dark, auto`): `light`, `dark`, or `auto` to follow the visitor's system.
- `data.settings.accent` (`string`, required, default: `#4F46E5`, pattern: `^#[0-9a-fA-F]{6}$`): The brand colour of the launcher and the buttons, as `#RRGGBB`.
- `data.settings.radius` (`enum<string>`, required, default: `medium`, one of: `none, small, medium, large`): How round the corners are: `none`, `small`, `medium` or `large`.
- `data.settings.bubbles` (`enum<string>`, required, default: `corners`, one of: `corners, round`): The shape of the visitor's message bubbles and the suggestion chips: `corners` (default) follows `radius`, `round` makes them fully round whatever the corners are. Omit it to leave it as it is.
- `data.settings.position` (`enum<string>`, required, default: `bottom-right`, one of: `bottom-right, bottom-left`): Which corner of the page the bubble sits in: `bottom-right` or `bottom-left`.
- `data.settings.launcher` (`enum<string>`, required, default: `bubble`, one of: `bubble, pill, bar`): `bubble`: a round button in a corner. `pill`: a button with `launcher_label`. `bar`: a bar centered at the bottom of the page, showing the `placeholder`, that opens the chat in the middle of the screen (`position` does not apply).
- `data.settings.launcher_label` (`string`, required, default: `Ask AI`, length: `≤ 40 chars`): The text of the pill launcher, up to 40 characters.
- `data.settings.launcher_icon` (`enum<string>`, required, default: `chat`, one of: `chat, message, help, sparkles, bot, search, book, bulb, bolt, heart, star, support`): The launcher's icon: `chat` (default), `message`, `help`, `sparkles`, `bot`, `search`, `book`, `bulb`, `bolt`, `heart`, `star` or `support`.
- `data.settings.launcher_size` (`enum<string>`, required, default: `medium`, one of: `small, medium, large`): The launcher's size, whatever its style: `small`, `medium` (default) or `large`.
- `data.settings.bar_icon` (`boolean`, required, default: `true`): The `bar` launcher: show the icon at its start (default). Off: a plain field.
- `data.settings.bar_translucent` (`boolean`, required, default: `false`): The `bar` launcher: see-through, with the page blurred behind it. Off (default): a solid bar.
- `data.settings.launcher_image` (`string | null`, required, default: `null`): Paid plans, read-only: the name of the image shown in place of the launcher's icon, set with `PUT /v1/widgets/{id}/images/launcher` or in the app. Null: the icon.
- `data.settings.favicon` (`string | null`, required, default: `null`): Paid plans, read-only: the name of the hosted page's favicon, set with `PUT /v1/widgets/{id}/images/favicon` or in the app. Null: a favicon drawn from the widget's colour and icon.
- `data.settings.header_icon` (`enum<string>`, required, default: `sparkles`, one of: `chat, message, help, sparkles, bot, search, book, bulb, bolt, heart, star, support, none`): The avatar in the chat's header, apart from the launcher's icon: `sparkles` (default), `chat`, `message`, `help`, `bot`, `search`, `book`, `bulb`, `bolt`, `heart`, `star` or `support`; `none` shows no avatar at all.
- `data.settings.header_image` (`string | null`, required, default: `null`): Paid plans, read-only: the name of the image shown in place of the icon in the chat's header, set with `PUT /v1/widgets/{id}/images/header` or in the app. Null: the header icon.
- `data.settings.title` (`string`, required, default: `""`, length: `≤ 60 chars`): The chat's title, up to 60 characters. Empty means the assistant's name.
- `data.settings.subtitle` (`string`, required, default: `""`, length: `≤ 80 chars`): A line under the chat's title, up to 80 characters, like `Available 24/7`. Empty (default): none, and the title sits centered alone. Omit it to leave it as it is.
- `data.settings.greeting` (`string`, required, default: `Hi! Ask me anything, I answer from our documentation.`, length: `≤ 500 chars`): The first message the visitor sees, up to 500 characters.
- `data.settings.placeholder` (`string`, required, default: `Ask a question…`, length: `≤ 80 chars`): The hint in the question box, up to 80 characters.
- `data.settings.suggestions` (`string[]`, required, default: `[]`, items: `≤ 4 items`): Up to 4 example questions (120 characters each) shown before the first question.
- `data.settings.disclaimer` (`string`, required, default: `AI answers can be wrong.`, length: `≤ 200 chars`): The small print under the chat, up to 200 characters. Empty hides it.
- `data.settings.language` (`enum<string>`, required, default: `auto`, one of: `auto, en, fr, de, es, it, pt`): The language of the widget's own words (buttons, notices): `auto` follows the visitor, or `en`, `fr`, `de`, `es`, `it`, `pt`. Answers always follow the visitor's language.
- `data.settings.sources` (`enum<string>`, required, default: `numbered`, one of: `numbered, numbered_no_links, inline_links, none`): How answers show their sources: `numbered` ([1] markers and a source list with links to web pages), `numbered_no_links` (the same without any address), `inline_links` (the answer links its own words to the source pages, no list; describe the style in the assistant's context) or `none`.
- `data.settings.memory` (`boolean`, required, default: `true`): Follow-up questions see the conversation so far.
- `data.settings.visitor_history` (`boolean`, required, default: `true`): Visitors find their past chats when they come back on the same browser (a random id kept in the chat frame's own storage). Off: nothing is kept in the browser and every visit starts fresh.
- `data.settings.feedback` (`boolean`, required, default: `true`): Thumbs up and down under each answer, with an optional word on what was wrong after a thumbs down. Read them in the workspace's logs: `GET /v1/logs/conversations?widget_id=…&rating=down` lists the conversations where visitors were not satisfied.
- `data.settings.branding` (`boolean`, required, default: `true`): Show "Powered by Lookio". Only paid plans may turn it off.
- `data.settings.bubble` (`boolean`, required, default: `true`): The chat bubble format: a launcher in a corner of the page that opens the chat. On by default. Off: the loader draws nothing.
- `data.settings.embed` (`boolean`, required, default: `false`): The in-page format: the chat inside a container of your page (`snippets.embed`, or `snippets.iframe`). Off by default; off, the frame answers 404.
- `data.settings.hosted_page` (`boolean`, required, default: `false`): The hosted page format: a full-page chat at `snippets.hosted`, to link to. Off by default; off, the page answers 404.
- `data.limits` (`object`, required)
- `data.limits.monthly_credits_enabled` (`boolean`, required, default: `false`)
- `data.limits.per_visitor_hour` (`integer`, required, default: `40`, range: `1–500`): Questions one visitor may ask per hour, 1 to 500.
- `data.limits.per_visitor_day` (`integer`, required, default: `150`, range: `1–2000`): Questions one visitor may ask per day, 1 to 2,000, and not fewer than the hourly limit.
- `data.limits.monthly_credits` (`integer | null`, required, default: `null`): Credits the widget may spend per calendar month (UTC), 10 to 10,000,000. Null: no ceiling beyond the workspace's balance. When it is reached the widget stops answering until the month turns.
- `data.snippets` (`object`, required)
- `data.snippets.bubble` (`string`, required): A `<script>` tag: a launcher in a corner of the page opens the chat. Paste it before `</body>`.
- `data.snippets.embed` (`string`, required): A `<div>` and the script: the chat fills the div, where you put it. Give the div a height.
- `data.snippets.hosted` (`string`, required): A link to a full-page chat. Needs no website; can be turned off with `settings.hosted_page`.
- `data.snippets.iframe` (`string`, required): A raw `<iframe>` for builders that refuse scripts (Notion, some CMS).
- `data.stats` (`object`, required): This calendar month, UTC.
- `data.stats.conversations` (`integer`, required)
- `data.stats.answers` (`integer`, required)
- `data.stats.credits` (`integer`, required)
- `data.created_via` (`string,null`, required)
- `data.created_at` (`string`, required)
- `data.updated_at` (`string`, required)
- `request_id` (`string`, required)

```json
{
  "data": {
    "id": "wdg_xpatjpwt22fu",
    "name": "Docs chat",
    "assistant_id": "ast_66xcnwtzj8vt",
    "assistant_name": "Support",
    "mode": "flash",
    "status": "live",
    "allowed_domains": [
      "docs.acme.com"
    ],
    "settings": {
      "theme": "auto",
      "accent": "#4F46E5",
      "radius": "medium",
      "bubbles": "corners",
      "position": "bottom-right",
      "launcher": "bubble",
      "launcher_label": "Ask AI",
      "title": "",
      "greeting": "Hi! Ask me anything, I answer from our documentation.",
      "placeholder": "Ask a question…",
      "suggestions": [],
      "disclaimer": "AI answers can be wrong.",
      "language": "auto",
      "sources": "numbered",
      "memory": true,
      "visitor_history": true,
      "feedback": true,
      "branding": true,
      "hosted_page": true
    },
    "limits": {
      "per_visitor_hour": 40,
      "per_visitor_day": 150,
      "monthly_credits": null
    },
    "snippets": {
      "bubble": "<script src=\"https://widget.lookio.app/v1/widget.js\" data-lookio-widget=\"wdg_j36h6kb8tyng\" async></script>",
      "embed": "<div data-lookio-embed=\"wdg_j36h6kb8tyng\" style=\"height: 600px\"></div>\n<script src=\"https://widget.lookio.app/v1/widget.js\" async></script>",
      "hosted": "https://widget.lookio.app/w/wdg_j36h6kb8tyng",
      "iframe": "<iframe src=\"https://widget.lookio.app/w/wdg_j36h6kb8tyng?embed=1\" style=\"width: 100%; height: 600px; border: 0\" title=\"Chat\"></iframe>"
    },
    "stats": {
      "conversations": 12,
      "answers": 31,
      "credits": 930
    },
    "created_via": "api",
    "created_at": "2026-10-12T09:30:00.000Z",
    "updated_at": "2026-10-12T09:30:00.000Z"
  },
  "request_id": "req_01K9X7M2Q4"
}
```

**400**: Possible codes: invalid_request, unknown_field, invalid_parameter, invalid_cursor

- `error` (`object`, required)
- `error.type` (`string`, required)
- `error.code` (`string`, required)
- `error.message` (`string`, required)
- `error.param` (`string`)
- `error.docs_url` (`string`, required)
- `error.action_url` (`string<uri>`)
- `request_id` (`string`, required)

```json
{
  "error": {
    "type": "<string>",
    "code": "<string>",
    "message": "<string>",
    "param": "<string>",
    "docs_url": "<string>",
    "action_url": "<string>"
  },
  "request_id": "<string>"
}
```

**401**: Possible codes: authentication_required, invalid_api_key, api_key_expired, api_key_revoked, invalid_access_key

- `error` (`object`, required)
- `error.type` (`string`, required)
- `error.code` (`string`, required)
- `error.message` (`string`, required)
- `error.param` (`string`)
- `error.docs_url` (`string`, required)
- `error.action_url` (`string<uri>`)
- `request_id` (`string`, required)

```json
{
  "error": {
    "type": "<string>",
    "code": "<string>",
    "message": "<string>",
    "param": "<string>",
    "docs_url": "<string>",
    "action_url": "<string>"
  },
  "request_id": "<string>"
}
```

**403**: Possible codes: insufficient_scope, feature_not_available, admin_required, permission_denied

- `error` (`object`, required)
- `error.type` (`string`, required)
- `error.code` (`string`, required)
- `error.message` (`string`, required)
- `error.param` (`string`)
- `error.docs_url` (`string`, required)
- `error.action_url` (`string<uri>`)
- `request_id` (`string`, required)

```json
{
  "error": {
    "type": "<string>",
    "code": "<string>",
    "message": "<string>",
    "param": "<string>",
    "docs_url": "<string>",
    "action_url": "<string>"
  },
  "request_id": "<string>"
}
```

**404**: Possible codes: resource_not_found

- `error` (`object`, required)
- `error.type` (`string`, required)
- `error.code` (`string`, required)
- `error.message` (`string`, required)
- `error.param` (`string`)
- `error.docs_url` (`string`, required)
- `error.action_url` (`string<uri>`)
- `request_id` (`string`, required)

```json
{
  "error": {
    "type": "<string>",
    "code": "<string>",
    "message": "<string>",
    "param": "<string>",
    "docs_url": "<string>",
    "action_url": "<string>"
  },
  "request_id": "<string>"
}
```

**409**: Possible codes: conflict, assistant_has_no_resources

- `error` (`object`, required)
- `error.type` (`string`, required)
- `error.code` (`string`, required)
- `error.message` (`string`, required)
- `error.param` (`string`)
- `error.docs_url` (`string`, required)
- `error.action_url` (`string<uri>`)
- `request_id` (`string`, required)

```json
{
  "error": {
    "type": "<string>",
    "code": "<string>",
    "message": "<string>",
    "param": "<string>",
    "docs_url": "<string>",
    "action_url": "<string>"
  },
  "request_id": "<string>"
}
```

**413**: Possible codes: request_too_large

- `error` (`object`, required)
- `error.type` (`string`, required)
- `error.code` (`string`, required)
- `error.message` (`string`, required)
- `error.param` (`string`)
- `error.docs_url` (`string`, required)
- `error.action_url` (`string<uri>`)
- `request_id` (`string`, required)

```json
{
  "error": {
    "type": "<string>",
    "code": "<string>",
    "message": "<string>",
    "param": "<string>",
    "docs_url": "<string>",
    "action_url": "<string>"
  },
  "request_id": "<string>"
}
```

**429**: Possible codes: rate_limited

- `error` (`object`, required)
- `error.type` (`string`, required)
- `error.code` (`string`, required)
- `error.message` (`string`, required)
- `error.param` (`string`)
- `error.docs_url` (`string`, required)
- `error.action_url` (`string<uri>`)
- `request_id` (`string`, required)

```json
{
  "error": {
    "type": "<string>",
    "code": "<string>",
    "message": "<string>",
    "param": "<string>",
    "docs_url": "<string>",
    "action_url": "<string>"
  },
  "request_id": "<string>"
}
```

**500**: Possible codes: internal_error

- `error` (`object`, required)
- `error.type` (`string`, required)
- `error.code` (`string`, required)
- `error.message` (`string`, required)
- `error.param` (`string`)
- `error.docs_url` (`string`, required)
- `error.action_url` (`string<uri>`)
- `request_id` (`string`, required)

```json
{
  "error": {
    "type": "<string>",
    "code": "<string>",
    "message": "<string>",
    "param": "<string>",
    "docs_url": "<string>",
    "action_url": "<string>"
  },
  "request_id": "<string>"
}
```

**Example request**

```bash
curl --request POST \
  --url 'https://api.lookio.app/v1/widgets' \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "assistant_id": "<string>"
}'
```
