---
title: "How to Serve a Typed AI Endpoint with Pixeltable"
date: "2026-09-25"
dateModified: "2026-09-25"
author: "Pierre Brunelle"
tags:
  - Serving
  - FastAPI
  - OpenAPI
  - TypeScript
  - Next.js
  - Pixeltable Cloud
  - API Keys
  - TableModel
description: "Turn a Pixeltable table into a typed HTTP endpoint: serve it locally, deploy it to Pixeltable Cloud, and call it from Next.js with generated TypeScript types."
url: "https://www.pixeltable.com/blog/serve-typed-ai-endpoint-pixeltable"
---

# How to Serve a Typed AI Endpoint with Pixeltable

**Summary:** A Pixeltable service turns tables into HTTP routes. You declare the table and its routes in one `app.py`, run two commands to serve it locally, and three to host it on Pixeltable Cloud. Every service publishes an OpenAPI schema, so a TypeScript app gets generated types for every route, and a hosted service accepts only requests that carry a Pixeltable API key. This post walks the whole path, from the table to a typed call in a Next.js app.

 
The reference docs are [HTTP serving](https://docs.pixeltable.com/howto/deployment/serving) and [Deploy to Pixeltable Cloud](https://docs.pixeltable.com/cloud). Everything below uses Pixeltable 0.7.11 or later. `pxt login`, `pxt whoami`, and `pxt key` need 0.7.10 or later. Configuration of the hosted database is in [Configure Pixeltable Cloud](/blog/configure-pixeltable-cloud).

 
## What a service gives you

 
A route is a table operation exposed over HTTP. You choose the operation and the columns; Pixeltable writes the handler, the request and response models, and the schema.

 
| Route | What a request does | Typical use |
| --- | --- | --- |
| add_insert_route | Inserts a row, runs its computed columns, returns the outputs you list | Ingest a document, an image, a transcript |
| add_compute_route | Runs the computed columns without storing a row | Classify, summarize, or embed on demand |
| add_update_route | Updates a row by key and recomputes what depends on it | Correct a field, rerun a model |
| add_delete_route, query routes | Delete rows, or return the rows a query selects | Cleanup, search, lookups |

 
Routes can accept file uploads, return a file instead of JSON (`return_fileresponse=True`), or run in the background and return a job handle (`background=True`).

 
## Declare the table and its routes

 
This is the Quickstart application. `pxt service example --out app.py` writes it for you.

 
```python
import pixeltable as pxt
import pixeltable.functions as pxtf
from pixeltable.serving import FastAPIRouter

TableModel = pxt.model_base()

@pxt.udf
def excerpt(text: str, n: int = 12) -> str:
 return text if len(text) <= n else f'{text[:n]}...'

class Docs(TableModel, name='docs'):
 id = pxt.Column(value=pxtf.uuid.uuid7(), primary_key=True)
 title: pxt.String
 body: pxt.String | None
 title_upper = pxtf.string.upper(title)
 summary = excerpt(title)

ingest = FastAPIRouter(name='ingest')
ingest.add_insert_route(
 Docs, path='/docs', inputs=[Docs.title, Docs.body], outputs=[Docs.id, Docs.title_upper, Docs.summary]
)
ingest.add_update_route(
 Docs, path='/docs/update', inputs=[Docs.title], outputs=[Docs.id, Docs.title_upper]
)
ingest.add_compute_route(Docs, path='/titles', inputs=[Docs.title], outputs=[Docs.title_upper])
```

 
Each column named in `outputs` becomes a typed field of the response, computed columns included. Swap `pxt.String` for `pxt.Image`, `pxt.Video`, `pxt.Audio`, or `pxt.Document` and the same routes serve a media pipeline.

 
## Serve it locally

 
```bash
pip install -U 'pixeltable[serve]'
pxt init
pxt schema update app.py my_app -f
pxt service update app.py my_app -f
pxt service list my_app --json
```

 
`pxt schema update` creates the tables; it does not start HTTP. `pxt service update` starts HTTP; it does not create tables. Pass `-f` whenever a script, CI job, or coding agent runs these commands: without a terminal to confirm in, commands that change things refuse to proceed.

 
The service gets its own port, so read the endpoint from `pxt service list --json` rather than assuming port 8000. Interactive docs are at `/docs` and the schema at `/openapi.json`. A local service needs no key.

 
## Deploy the same file to Pixeltable Cloud

 
`pxt new` creates a free hosted database with no account and prints a claim link. A `pxt login` session is enough for this deployment if you already have an organization. You do not need an API key until a backend calls the hosted service. If a key is set in the environment or in the config file, it takes precedence over the login session, so remove a stale one before you sign in.

 
Name the hosted database in `pixeltable.toml`. Keep the local entry `pxt init` wrote. If `pxt init` wrote `pyproject.toml`, use `[[tool.pixeltable.database]]` instead. `pxt org create` provisions the first database as `main`, so a new organization uses `pxt://yourorg:main`. The examples below use `pxt://acme:main`; replace both parts with yours. What the entry controls, including when the image rebuilds, is in [Configure Pixeltable Cloud](/blog/configure-pixeltable-cloud).

 
```toml
[[pixeltable.database]]
name = 'pxt://acme:main'
```

 
Wait until `pxt db status` shows `AVAILABLE`. Until then, `pxt db update` is refused. Then run these commands from the project directory. Add `-f` when there is no terminal to confirm in:

 
```bash
pxt db status pxt://acme:main
pxt db update pxt://acme:main -f
pxt schema update app.py pxt://acme:main -f
pxt service update app.py pxt://acme:main -f
```

 

 - `pxt db update` uploads the project onto the database and updates its image and size. It does not insert rows and it does not start endpoints. It rebuilds the image only when the environment changed, which is the slow step. A code change is an upload. The first update takes several minutes.

 - `pxt schema update` creates the tables. It does not start HTTP.

 - `pxt service update` starts the hosted service. `pxt service run` is local only.

 

 
The hosted image installs the Pixeltable version your lockfile (`uv.lock` or `requirements.txt`) pins, so pin 0.7.11 or later there too. `pxt service list pxt://acme:main` prints the service URL. The host looks like `https://{org}-{db}.svc.pxt.run/<service>`. Copy it from the command. Do not hardcode it, and do not send service traffic to the control-plane host. `pxt service diff app.py pxt://acme:main` shows what would change before you apply it. The docs list [what to run after each kind of change](https://docs.pixeltable.com/cloud#what-to-run-after-a-change).

 
## Give your app its own key

 
Your app server needs a key that does not expire and does no more than the app needs. Create one per app and environment:

 
```bash
pxt key create my-app --grant access:pxt://acme:main/services/ingest
```

 
With the grant, the key belongs to the organization and can call the `ingest` service and nothing else. It cannot list services, reconfigure them, or reach another database. Keys with grants are a preview; until your organization has them, omit `--grant` for a key that acts as you. The secret prints once. Store it in your app server's environment as `PIXELTABLE_API_KEY`, never under a `NEXT_PUBLIC_` name that would ship it to the browser. See [`pxt key`](https://docs.pixeltable.com/platform/cli#pxt-key) for the full grant table.

 
## Generate TypeScript types from the schema

 
A hosted schema needs the key to download. It declares the `X-api-key` header on every route, so generated clients know how to authenticate:

 
```bash
export PIXELTABLE_SERVICE_URL='https://…' # the endpoint from pxt service list
curl -fsS -H "X-api-key: $PIXELTABLE_API_KEY" "${PIXELTABLE_SERVICE_URL%/}/openapi.json" -o openapi.json
npx openapi-typescript openapi.json -o src/pixeltable.d.ts
```

 
File responses are typed as binary, and the background job status route is in the schema, so polling is typed too. Regenerate the file whenever the Python routes change, and let your TypeScript check catch any mismatch before you deploy.

 
## Call it from a Next.js server

 
Generated types do not check a raw `fetch`, so call routes through `openapi-fetch`. Run `npm install openapi-fetch server-only` and add `src/lib/pixeltable-client.ts`:

 
```typescript
import 'server-only';
import createClient from 'openapi-fetch';
import type { paths } from '../pixeltable';

const apiKey = process.env.PIXELTABLE_API_KEY;

export const pixeltable = createClient<paths>({
 baseUrl: process.env.PIXELTABLE_SERVICE_URL,
 headers: apiKey ? { 'X-api-key': apiKey } : {},
 cache: 'no-store',
});

export class PixeltableError extends Error {
 status: number;
 body: unknown;
 constructor(status: number, body: unknown) {
 super(`Pixeltable service returned HTTP ${status}`);
 this.status = status;
 this.body = body;
 }
}

export async function computeTitle(title: string) {
 const { data, error, response } = await pixeltable.POST('/titles', { body: { title } });
 if (error !== undefined || !response.ok) throw new PixeltableError(response.status, error);
 return data; // typed from the service schema
}
```

 
The key is set once on the client, so no call has to pass it. `import 'server-only'` makes the build fail if a client component imports the module. Call it from a Server Action:

 
```typescript
// src/app/actions.ts
'use server';
import { computeTitle } from '@/lib/pixeltable-client';

export async function titleCase(title: string) {
 // check your own user session here before exposing a write route
 const result = await computeTitle(title);
 // compute routes are typed nullable: a view filter can drop the row
 return result?.title_upper ?? null;
}
```

 
The same module calls a local service: point `PIXELTABLE_SERVICE_URL` at the local endpoint and leave `PIXELTABLE_API_KEY` unset. A deployment on Vercel or any other host needs the hosted endpoint, since it cannot reach your laptop. In the Cloud dashboard, the service page's **API docs** lets you try every route as your signed-in user, and **Use this endpoint** gives server-side snippets.

 
## Handle errors and retries

 
`PixeltableError.body` is the parsed error response. Branch on the shape of its `detail`, not on the status:

 
| detail is | When |
| --- | --- |
| An object with error_code, message, retryable, sometimes retry_after | A Pixeltable runtime error, or an error the Cloud gateway answers itself |
| A string | A missing row |
| An array | A request that failed validation |

 
The gateway answers these itself, with the same object:

 
| Status | Meaning | Retry? |
| --- | --- | --- |
| 401 | Missing or invalid key | No: fix the key |
| 403 | The key has no access to this service | No: add a grant |
| 404 | No service at this URL | No: check the endpoint |
| 429 | The key is rate limited | Yes, after Retry-After |
| 503 | The service is not running | Yes |

 
Retry a write only when `detail.retryable` is true, waiting `retry_after` seconds when it is present. That one rule covers service and gateway errors alike.

 
## Files, uploads, and background jobs

 

 - **File responses.** A `return_fileresponse=True` route needs `parseAs: 'blob'`, or `parseAs: 'stream'` to pipe the body through your Route Handler with the upstream `Content-Type`. Without it the client tries to parse the file as JSON.

 - **Uploads.** Generated types describe upload fields as strings, so send uploads with `fetch`, a `FormData` body, and the same `X-api-key` header. Let `fetch` set the multipart boundary.

 - **Background jobs.** A `background=True` route returns an `id` and a `job_url` right away. Polling is a typed call on the same client: `pixeltable.GET('/_pxt/jobs/{job_id}', { params: { path: { job_id: id } } })` returns `pending`, `done` with the result, or `error` with an `error_detail` that follows the same retry rule.

 - **Media outputs.** Image and video URLs in a hosted JSON response are signed and expire after an hour. Render them directly; do not store them.

 

 
## Secrets, logs, and changes

 
Provider keys such as `OPENAI_API_KEY` are secrets, not API keys. Set them in the dashboard or with the CLI. A running process keeps the values it started with, so restart what should read them: the database for work on its tables, and each service for its routes.

 
```bash
pxt secret set pxt://acme:main OPENAI_API_KEY=...
pxt db restart pxt://acme:main
pxt service restart pxt://acme:main/ingest
pxt service logs pxt://acme:main/ingest
```

 
A database secret overrides an organization secret of the same name. After a code change, run `pxt db update` and then `pxt service update`; after a column change, add `pxt schema update` between them.

 
## The checklist

 

 - Pin Pixeltable 0.7.11 or later in the project's lockfile.

 - Serve locally first, with `-f` in anything scripted.

 - Sign in with `pxt login`, or export one API key; a stored key wins over the login.

 - Wait until `pxt db status` shows `AVAILABLE`, then run `pxt db update`, `pxt schema update`, and `pxt service update`.

 - Create one key per app and environment, granted only the service it calls.

 - Generate types from the hosted schema and regenerate after every route change.

 - Call the service from server code only, through the typed client.

 - Check from outside: a request without the key returns 401 with a JSON `detail`, and one with the key returns 200.

 - Retry only when `detail.retryable` is true.

 - Set provider secrets, then restart the database and each service.

 

 
Full reference: [HTTP serving](https://docs.pixeltable.com/howto/deployment/serving), [Deploy to Pixeltable Cloud](https://docs.pixeltable.com/cloud), and the [CLI](https://docs.pixeltable.com/platform/cli). The database entry, image rebuilds, secrets, and keys are in [Configure Pixeltable Cloud](/blog/configure-pixeltable-cloud). For why the same file runs locally and in the cloud, read [why the local-cloud loop matters](/blog/why-local-cloud-loop-matters).
## Frequently asked questions

### How do I serve a Pixeltable table as an HTTP API?

Declare the table as a TableModel in app.py and add routes with FastAPIRouter. Locally, run pxt schema update and then pxt service update. On Pixeltable Cloud, wait until pxt db status shows AVAILABLE, then run pxt db update, pxt schema update, and pxt service update against pxt://org:main. pxt db update uploads the project. It creates the database when it does not exist yet, and it does not start HTTP.

### How do I get TypeScript types for a Pixeltable endpoint?

Every service publishes an OpenAPI schema at /openapi.json. Download it (with your API key when hosted) and run npx openapi-typescript openapi.json -o src/pixeltable.d.ts, then call routes through openapi-fetch so request bodies and responses are type-checked.

### How do I authenticate calls to a hosted Pixeltable service?

Send a Pixeltable Cloud API key in the X-api-key header. Create one in the dashboard or with pxt key create; with --grant access:pxt://org:db/services/name the key can call only that service.

### Can I call a Pixeltable service from the browser?

Not with your API key. Keep the key in your application server environment and call the service from Server Components, Route Handlers, or Server Actions. The browser calls your own server.

### How should my app handle Pixeltable errors and retries?

Errors carry a JSON detail. Pixeltable runtime errors and errors from the Cloud gateway use an object with error_code, message, and retryable. Retry a write only when retryable is true, after retry_after seconds when it is present.

