# Query the web with SQL and get a table with Keenable SELECT

Keenable SELECT turns web pages into a table: one read-only SQL query reads the pages, pulls named fields out of each one and returns rows instead of links. Each query that answers 200 costs $0.10 per request from Credits, and a query that fails costs nothing.

## At a glance

- **Endpoint:** `POST https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/v1/zeroclick/select`
- **Body:** `{"query": "<one read-only SELECT>"}`, plus optional `show_preview` (boolean, default `false`)
- **Dialect:** DuckDB SQL with `WEB_FETCH`, `WEB_SEARCH`, `SEM_EXTRACT`, `SEM_EXTRACT_ALL`, `SEM_MATCH`, `SEM_SCORE` and `SEM_NORM`
- **Price:** $0.10 per request for each query that answers 200. A query that fails is not billed.
- **Payment:** calls draw on the Credits plan. You buy Credits once, from $0.50, over x402, MPP or card.
- **Response:** `result_set_id`, `total_rows`, `columns`, `rows` or a `preview`, and `notes`. The charge and remaining credit are in the `zc-billing` header.
- **Full purchase flow:** [the buyer skill](https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/SKILL.md). Query syntax: [the query guide](https://select.keenable.ai/guide.md).

## Run a query over known pages

1. Confirm that your credential holds Credits:

   ```bash
   curl -s https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/agent/entitlements \
     -H "Authorization: Bearer $ACCESS_TOKEN"
   ```

   The response lists your plans with `remainingCreditUsd`. If the list has no Credits plan, buy Credits through [the buyer skill](https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/SKILL.md); [auth.md](https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/auth.md) covers registering the credential.

2. Write the query. This one reads three known pages with `WEB_FETCH` and extracts two named fields into one object per row:

   ```sql
   SELECT
     url,
     SEM_EXTRACT(
       content,
       'a Keenable blog post',
       product := 'the Keenable product or feature the post is about',
       partner := 'company the post names as a partner or integration, if any'
     ) AS post
   FROM WEB_FETCH(
     'https://keenable.ai/blog/<post-1>',
     'https://keenable.ai/blog/<post-2>',
     'https://keenable.ai/blog/<post-3>'
   )
   ```

3. Send the query as the `query` string:

   ```bash
   jq -n --rawfile q select.sql '{query: $q}' | \
   curl -s -X POST https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/v1/zeroclick/select \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d @-
   ```

   The response is a table with one row per page and a `post` object holding `product` and `partner`. Abridged:

   ```json
   {
     "result_set_id": "rbcf147e3426",
     "total_rows": 3,
     "columns": ["post", "… 1 more"],
     "rows": [
       {"url": "https://keenable.ai/…", "post": {"product": "SELECT", "partner": null}},
       "… 2 more"
     ],
     "notes": [{"text": "Fetch result set: r24da7a1cb1c (3 rows), reference it as FROM r24da7a1cb1c in fo…"}]
   }
   ```

   A field is `null` where the page does not state it. The note names a fetch result set and its id. This query drew $0.10 from Credits, as stated in the `zc-billing` header.

## Filter the saved result set

Every output is saved as a result set. A later query reads it by putting its `result_set_id` in `FROM`. That query uses plain SQL and fetches no new pages:

```sql
SELECT
  post.partner AS partner,
  post.product AS product,
  url
FROM rbcf147e3426
WHERE post.partner IS NOT NULL
ORDER BY partner
```

This follow-up returns the two pages that name a partner, with a new `result_set_id` of its own. Each row holds `partner`, `product` and `url`. It answered in 0.8 seconds and drew $0.10 from Credits. A follow-up is billed like any other query that answers 200.

## Search the web instead of fetching

Use `WEB_SEARCH` when you do not know the URLs. It runs several search queries, merges the results and removes duplicate URLs. Each result becomes a row with `url`, `title`, `content`, `published_at` and `query`. Adapt this form:

```sql
SELECT url, UNNEST(
  SEM_EXTRACT_ALL(
    content,
    'an open-source vector database',
    name := 'project name',
    license := 'software license name',
    latest_release := 'date of the latest release the page states, YYYY-MM-DD'
  ),
  recursive := true
)
FROM WEB_SEARCH(
  'open source vector database comparison',
  'self-hosted vector database benchmark',
  'site:github.com vector database release notes',
  'Milvus Qdrant Weaviate new release',
  'embedding store for RAG open source license',
  'site:reddit.com which vector database in production',
  published_after := '1mo'
)
WHERE SEM_MATCH(content, 'describes a specific open-source vector database')
```

The form follows these rules:

- Give at least six queries that vary in wording, source type and angle. Include queries that name specific members of a category.
- Use `published_after` only when recency matters. A date filter drops undated pages.
- Use `SEM_MATCH` in `WHERE` to keep only the relevant pages.

A query like this reads many pages and takes longer than one search call. Give your client a long timeout.

## Write queries that run

| Rule | Reason |
| --- | --- |
| Never select `content` whole, bare or through `*` | The query is rejected. Extract fields instead, use `substr(content, 1, 10000)`, or use `* EXCLUDE (content)`. |
| Put format hints in field descriptions (`'date, YYYY-MM-DD'`) | The extractor returns values in that format. |
| Never put an example answer in a description | The extractor copies the example's wording into every value. |
| Set `show_preview` to `true` to check the shape | The response then holds a 3-row preview instead of every row. |
| Expect a preview when the rows are too many | The full table stays readable through its `result_set_id`. |
| Read `FROM` a result set id, `WEB_SEARCH` or `WEB_FETCH` only | Other tables and `FROM`-less queries are not accepted. |

## Limits and charges

- One read-only `SELECT` per request. No writes and no other statements.
- Extraction reports what each page says and does not check it. Cross-check precise values across independent pages.
- Only a query that answers 200 is charged. Ten queries cost $1.

## Errors

| Status or code | Meaning | Action |
| --- | --- | --- |
| `400` | The query or body is invalid, or the query failed. Nothing was charged. | Read the error and fix the query |
| `401 bearer_required` | No agent credential was sent | Register at [auth.md](https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/auth.md) and send `Authorization: Bearer` |
| `403 plan_required` | The credential holds no Credits (`reason`: `access_not_found`) | Buy Credits through the plan in the body's `plans` list |
| `403 plan_required`, `reason`: `usage_exhausted` | The Credits balance is used up | Top up with `POST https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/extend`, or buy again |
| `429` | Too many of your queries are running. Nothing was charged. | Retry shortly |
| `502` | The query could not run. Nothing was charged. | Retry |
| `503` | The allowance could not be confirmed. Nothing was charged. | Retry after `Retry-After` |

## Frequently asked questions

### Does a follow-up query on a saved result set cost anything?

Yes. Every query that answers 200 is billed $0.10 per request, including one that only reads a saved result set. Such a query fetches no new pages.

### What does SELECT return when a page does not state a field?

The field is `null` in that row. You can filter those rows in SQL, for example with `WHERE post.partner IS NOT NULL`.

### Can I pay for a single query without buying Credits?

No. Every call draws on Credits held by your credential. A call without a credential answers `401 bearer_required`, and a call from a credential without Credits answers `403 plan_required`.

### When should I use WEB_FETCH and when WEB_SEARCH?

Use `WEB_FETCH` for pages whose URLs you already know or have collected. Use `WEB_SEARCH` to find pages across six or more varied search queries.

## Keep reading

- [Buyer skill](https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/SKILL.md): the full purchase flow
- [Query guide](https://select.keenable.ai/guide.md): the dialect, functions and examples
- [SELECT API reference](https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/services/select/openapi.json): request and response schema
- [How to pay](https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/payment.md): Credits, rails and top-ups
- [Error reference](https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/errors.md): every error code

---

Updated 2026-11-04. All Keenable guides: https://zeroclick.keenable.ai/guides.md. Storefront: https://zeroclick.keenable.ai/zcj/bg3rzu1q6il0/llms.txt.
