> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Poll for agent replies

GET https://api.jelliu.co/widget/poll

Returns agent-side messages (AI replies and human operator replies from the dashboard) created at or
after `since`, oldest first, at most 50 per call. Visitor messages are not included, because the UI
already shows what the visitor typed. Before the visitor's first message there is no conversation
and `messages` is empty.

Loop: pass the returned `now` as the next `since`. The boundary is inclusive, so a message can come back
twice; de-duplicate by `id` (and against `message_id` from `POST /widget/message`). If 50 messages come
back, poll again immediately with `since` set to the last message's `created_at`.

Credentials, origin check and limits are the same as `POST /widget/init`. The session must belong to
the widget named by the credential. Each poll counts against the per-IP and per-widget budgets, so
poll every few seconds, not continuously.

**Idempotency.** Read-only; safe to retry.

**Access**
- **Required scope:** None; `jl_` API keys are not used. The widget credential plus a matching `x-widget-parent-origin` authorise the call.
- **Rate limit:** Widget public — 60 requests/min per IP, plus the widget's `rate_limit_rpm` per credential and 300 requests/min per workspace; the general limiter also counts 120 requests/min per IP. See [Rate limits](/rate-limits).
- **Plan:** Available on every plan.


Reference: https://developer.jelliu.co/api-reference/widgets/get-widget-poll

## Request

### Query parameters

- `wid` (string, optional) — Public widget id as a query parameter. Used when neither header is present.
- `session_id` (string, required) — Session id from `POST /widget/init`. Not a UUID returns 400 `Invalid query`.
- `since` (datetime, optional) — ISO 8601 UTC timestamp (for example the previous response's `now`). Messages created at or after it are returned. Omitted means from the start of the conversation. A value that is not an ISO datetime with `Z` returns 400 `Invalid query`.

### Headers

- `x-widget-api-key` (string, optional) — Secret widget API key (`plaintext_api_key` from `POST /api/widgets`). Takes precedence over `x-widget-id` and `wid`. Send one of the three. Never expose it in a browser.
- `x-widget-id` (string, optional) — Public widget id, the credential the embed script uses. Used when `x-widget-api-key` is absent.
- `x-widget-parent-origin` (string, required) — Origin of the page embedding the widget. Must match one of the widget's `allowed_origins`; see `POST /widget/init`.

## Response

### 200

New agent messages (possibly none).

- `data` (object, required)
  - `messages` (list of object, required) — Agent-role messages, oldest first, at most 50.
    - `id` (string, optional) — Message id. Use it to de-duplicate across message, poll and WebSocket deliveries.
    - `role` (enum, optional) — Who wrote it. `user` is the visitor; `agent` is the AI agent or a human operator replying from the dashboard (they are not distinguished). `GET /widget/poll` only returns `agent`.
      - Allowed values: `user`, `agent`
    - `content` (string, optional) — Message text.
    - `created_at` (datetime, optional) — When the message was stored.
  - `now` (datetime, required) — Server time taken before the read; use it as the next `since`.

## Errors

### 400 Bad Request Error

`session_id` missing or not a UUID, or `since` not an ISO datetime.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 401 Unauthorized Error

`Missing widget credential` or `Invalid widget API key` (see `POST /widget/init`).

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 403 Forbidden Error

Widget deactivated, workspace deleted, or origin missing / invalid / not allowed (see `POST /widget/init`).

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 404 Not Found Error

The session does not exist or belongs to another widget.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 429 Too Many Requests Error

A per-minute limit was hit (see `POST /widget/init` for the messages).

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

### 503 Service Unavailable Error

The per-widget limiter's store is unreachable; it fails closed.

- `error` (object, required) — The error object. Always has `code` and `message`.
  - `code` (string, required) — Stable machine-readable error code (for example `VALIDATION_FAILED`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BILLING_ERROR`, `COMPLIANCE_BLOCKED`, `RATE_LIMIT_EXCEEDED`, `INTERNAL_ERROR`). Switch on this, not on `message`. See [Errors](/errors).
  - `message` (string, required) — Human-readable explanation. English or Spanish depending on the route; may change without notice.
  - `details` (object or list of object, optional) — Present on `VALIDATION_FAILED` only. Its shape depends on how the route validates: * a field map, either Zod's `flatten()` output (`{ "formErrors": [], "fieldErrors": { "name": ["..."] } }`) or just its `fieldErrors` part (`{ "name": ["..."] }`); * an issue list, where each issue has at least `path`, `message` and `code`. `path` is a dot-separated string on routes that let the schema throw, and an array of keys on routes that forward Zod's raw issues (those also carry Zod's extra issue fields).
    - Field map
      - `formErrors` (list of string, optional)
      - `fieldErrors` (map from string to list of string, optional)
  - `metadata` (map from string to any, optional) — Structured detail exposed for a small allowlist of codes only — for example `BILLING_ERROR` carries `limit`, `current` and `tier` (resource caps) or `tier` and `feature` (feature gates).

## Examples

**Response**

```json
{
  "data": {
    "messages": [
      {
        "id": "33905cc7-5022-4ba0-b376-231c0bdd7f83",
        "role": "agent",
        "content": "Hola María, soy Carolina del equipo de recepción. Ya te reservé el sábado a las 9:00.",
        "created_at": "2026-09-15T13:20:06.118Z"
      }
    ],
    "now": "2026-09-15T13:20:09.771Z"
  }
}
```

**SDK Code**

```python Widgets_getWidgetPoll_example
import requests

url = "https://api.jelliu.co/widget/poll"

querystring = {"session_id":"b3e227dd-a9ea-48ce-9afe-09c21a5d8d2d","since":"2026-09-15T13:20:04.512Z","wid":"84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e"}

headers = {
    "x-widget-api-key": "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284",
    "x-widget-id": "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e",
    "x-widget-parent-origin": "https://www.clinicasonrisa.co"
}

response = requests.get(url, headers=headers, params=querystring)

print(response.json())
```

```javascript Widgets_getWidgetPoll_example
const url = 'https://api.jelliu.co/widget/poll?session_id=b3e227dd-a9ea-48ce-9afe-09c21a5d8d2d&since=2026-09-15T13%3A20%3A04.512Z&wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e';
const options = {
  method: 'GET',
  headers: {
    'x-widget-api-key': '3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284',
    'x-widget-id': '84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e',
    'x-widget-parent-origin': 'https://www.clinicasonrisa.co'
  }
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Widgets_getWidgetPoll_example
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.jelliu.co/widget/poll?session_id=b3e227dd-a9ea-48ce-9afe-09c21a5d8d2d&since=2026-09-15T13%3A20%3A04.512Z&wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("x-widget-api-key", "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284")
	req.Header.Add("x-widget-id", "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")
	req.Header.Add("x-widget-parent-origin", "https://www.clinicasonrisa.co")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Widgets_getWidgetPoll_example
require 'uri'
require 'net/http'

url = URI("https://api.jelliu.co/widget/poll?session_id=b3e227dd-a9ea-48ce-9afe-09c21a5d8d2d&since=2026-09-15T13%3A20%3A04.512Z&wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["x-widget-api-key"] = '3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284'
request["x-widget-id"] = '84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e'
request["x-widget-parent-origin"] = 'https://www.clinicasonrisa.co'

response = http.request(request)
puts response.read_body
```

```java Widgets_getWidgetPoll_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://api.jelliu.co/widget/poll?session_id=b3e227dd-a9ea-48ce-9afe-09c21a5d8d2d&since=2026-09-15T13%3A20%3A04.512Z&wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")
  .header("x-widget-api-key", "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284")
  .header("x-widget-id", "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")
  .header("x-widget-parent-origin", "https://www.clinicasonrisa.co")
  .asString();
```

```php Widgets_getWidgetPoll_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.jelliu.co/widget/poll?session_id=b3e227dd-a9ea-48ce-9afe-09c21a5d8d2d&since=2026-09-15T13%3A20%3A04.512Z&wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e', [
  'headers' => [
    'x-widget-api-key' => '3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284',
    'x-widget-id' => '84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e',
    'x-widget-parent-origin' => 'https://www.clinicasonrisa.co',
  ],
]);

echo $response->getBody();
```

```csharp Widgets_getWidgetPoll_example
using RestSharp;

var client = new RestClient("https://api.jelliu.co/widget/poll?session_id=b3e227dd-a9ea-48ce-9afe-09c21a5d8d2d&since=2026-09-15T13%3A20%3A04.512Z&wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e");
var request = new RestRequest(Method.GET);
request.AddHeader("x-widget-api-key", "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284");
request.AddHeader("x-widget-id", "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e");
request.AddHeader("x-widget-parent-origin", "https://www.clinicasonrisa.co");
IRestResponse response = client.Execute(request);
```

```swift Widgets_getWidgetPoll_example
import Foundation

let headers = [
  "x-widget-api-key": "3462145765a6b6c238f79a1e13e10361c24d1740ec30d27bea22dd4106c04284",
  "x-widget-id": "84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e",
  "x-widget-parent-origin": "https://www.clinicasonrisa.co"
]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.jelliu.co/widget/poll?session_id=b3e227dd-a9ea-48ce-9afe-09c21a5d8d2d&since=2026-09-15T13%3A20%3A04.512Z&wid=84b5aa0e-f6d6-4d88-b3ad-cd7a5a2aeb9e")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```