# Specta REST API & Webhooks (v3.0)

Specta ships a JSON REST API and signed webhooks so supplier catalogs can be queued, watched
and exported without anyone sitting in front of the screen.

Specta uses **your own** LLM API key. Extracted data stays on your server; each document goes only to the
provider you configure (with a local Ollama model, it never leaves your server).

## Authentication

Create a key in **API & Webhooks** (in the sidebar). Send it on every call:

    Authorization: Bearer apk_...

The key is shown once at creation and stored as a SHA-256 hash. Revoke any time.

## Endpoints

| Method & path | What it does |
|---|---|
| `GET  /api/ping` | Verify a key |
| `GET  /api/batches?status=` | Batches with roll-up status and per-status counts |
| `GET  /api/batches/{id}` | One batch with every queued file and its state |
| `GET  /api/catalogs?status=` | The catalogs |
| `POST /api/catalogs` | Queue a supplier document (multipart/form-data) |
| `GET  /api/exports?catalog_id=&preset=&format=` | Export a catalog |
| `GET  /api/openapi.json` | OpenAPI 3 spec (no auth) |

A key acts with the role of the person it belongs to: a viewer's key reads, and
`POST /api/catalogs` answers it `403` with the permission it lacks (`catalog.edit`).

### Batches are the async part

A batch is the one genuinely long-running thing here: you queue twenty supplier PDFs, the run
screen chews through them one file at a time, and you go and do something else. `GET /api/batches`
gives you each batch's rolled-up status — `processing`, `attention`, `extracted`, `reviewed` or
`empty` — plus the counts behind it (`queued`, `extracting`, `extracted`, `failed`, `reviewed`,
`total`, `done`, `remaining`).

That status is **the app's own roll-up**, not a second opinion: `attention` means at least one
file failed, `reviewed` means every file has been signed off, and a batch is never reported done
while anything is still queued.

### Queue a document

    curl -X POST https://your-install/api/catalogs \
      -H "Authorization: Bearer apk_..." \
      -F "file=@supplier-2026.pdf" -F "supplier=Acme Ltd" -F "schema_id=3"

`multipart/form-data`, because JSON cannot carry a PDF. Same 20 MB cap, same MIME allow-list and
same template fallback as the upload form — and, like the form, **the MIME type is read from the
file's bytes, never from the header the client sends**.

**Extraction stays a separate, explicit step.** Queuing a document does not spend anything;
extraction spends *your* LLM credit, and nothing here should spend that on your behalf without
you asking. Trigger it from the catalog screen or the batch run.

### Export

    GET /api/exports?catalog_id=12&preset=woocommerce
    GET /api/exports?catalog_id=12&format=json

`preset` is `plain`, `woocommerce` or `shopify`. Add `reviewed=1` to export only the products a
person has reviewed (3.2.0). This serves the **same bytes** the export
buttons serve — including a template's saved column mapping when one exists for that platform, at
the same precedence — so a nightly job and a human clicking Export cannot produce two different
files for the same catalog.

`format=json` returns the products as structured data instead, each carrying `needs_review`
computed from the same confidence threshold the review screen uses.

Since 3.2.0 each product also carries `image_url`, `variant_group_id` (0 when it is not in a
confirmed group), `written_state` (`""`, `generated` or `approved`) and `written`: the title and
description written from its confirmed facts, **only once a person has approved them** (`null`
before). The MCP tools return the same fields.

## Webhooks

Add receiver URLs in **API & Webhooks**. Events:

- `batch.completed` — a batch leaves `processing`, whether it finished cleanly or with failures
- `catalog.extracted` — a catalog has been read: a single upload's extraction, or each file a batch
  reads. The payload carries the catalog and its `products` count

`batch.completed` fires **exactly once**, on the transition out of `processing`. The run screen
polls after every file; an alert that arrives twenty times is an alert nobody reads. A batch that
ends with failures still completes — `attention` is a finished state, not a limbo — and the
payload carries the full summary so a receiver can decide whether anyone needs waking up.

Each delivery is an HTTP POST with a JSON body and:

    X-Specta-Event: batch.completed
    X-Specta-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret>

Verify the signature before trusting a payload. Delivery is one attempt with a short timeout; the
**Recent deliveries** log shows every attempt and its response code. Design receivers to be
idempotent.

## Email alerts

Set an SMTP server and a notification address in **Settings** to get a mail when a batch finishes,
including how many files failed and a link straight to the batch.

## Backup

`/backup.json` exports every table with **both** the SMTP password and **your LLM API key**
redacted.

## Why there is no product import

Products in Specta exist only as *extraction output*, scored field by field against a template
and carrying a confidence value that drives the review heat-map. There is no manual
product-entry screen to mirror, and importing a CSV of finished products would bypass the
template + confidence model that is the entire reason the product exists. The import path here is
a supplier document; the schema is the thing you configure.

## Limits, honestly

- The API acts at user level; there is no separate scope system.
- List endpoints cap at 200 rows.
- One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
- Extraction and review stay in the UI — both are judgment calls that spend money or attention.
- Extraction quality is your model's, not Specta's. The confidence scores tell you where to look.

---

# Own It 3.0 — the agent endpoint and the rest

## MCP — Specta for an AI assistant

Specta speaks **MCP** (Model Context Protocol) on one route, so an assistant can review a normalized
catalog, find what is incomplete and pull export-ready rows — without anyone writing an integration.

    POST /mcp
    Authorization: Bearer <your API key>   ← the SAME revocable key the REST API uses
    Content-Type: application/json

`GET /mcp` answers **405** with `Allow: POST`, so a misconfigured client is told what to do rather
than left guessing at a 404.

| Tool | Writes? | What it does |
|---|---|---|
| `list_catalogs` | no | Catalogs with status, supplier and schema |
| `catalog_detail` | no | One catalog, its schema columns and a **completeness summary** |
| `list_items` | no | Products; `incomplete_only` returns just the ones missing required attributes |
| `item_detail` | no | One product: every attribute, its confidence, and what is missing |
| `export_rows` | no | The catalog as export-ready rows, mapped through the schema's preset |
| `batch_status` | no | Per-file progress of a batch run |

### Connect it to an assistant

Mint the key in the app first: **Settings → API keys**. Choose **Read only** when the assistant should
answer questions but never change anything — the endpoint then lists only the read tools and refuses the
rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

| | |
|---|---|
| **The address** | `https://your-install/mcp` |
| **The key** | header `Authorization: Bearer apk_xxxx` |
| **The transport** | MCP over streamable HTTP, stateless |

**Claude** — one command, or the same URL and header as a custom connector in the desktop and web apps:

```sh
claude mcp add --transport http specta https://your-install/mcp \
  --header "Authorization: Bearer apk_xxxx"
```

**ChatGPT and the OpenAI API** — one entry in the Responses API's `tools` array (in ChatGPT itself, the
same URL and key go in as a connector):

```json
{
  "type": "mcp",
  "server_label": "specta",
  "server_url": "https://your-install/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}
```

**Every other client spells the same three facts differently — copy the shape from its own
documentation, not from here.** VS Code is the clearest example of why: its
[configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)
(read 6 September 2026) puts servers in `.vscode/mcp.json` under a **`"servers"` object** — *"an
object that maps server names to their configurations"*. Pasted as-is,
a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around
them belongs to whichever client you are configuring.

**A local model, n8n, or your own code** — n8n's **MCP Client** node takes the URL and the same
`Authorization: Bearer` header; a model running on your own machine reaches it through any MCP client, so
nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

```sh
curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet:
Specta implements the protocol, not an integration with a particular vendor.

### No tool can start an extraction

This is the refusal the endpoint is designed around, and it is deliberate rather than missing.

An extraction run calls **your** AI provider on **your** key. An agent that could trigger one could
spend real money without a person deciding to — a retry loop, an over-eager assistant re-running a
catalog "to be helpful", or simply a misunderstanding. Starting a run is a decision with a price
attached, so it stays a button in the browser.

`batch_status` is read-only on purpose: an agent can *watch* a run it did not start, report on it and
tell you when it finishes. Every tool in the table above is declared `readOnly`.

### Completeness is first-class over the API

`catalog_detail` returns the same roll-up the Completeness screen shows:

```json
"completeness": {
  "total": 120, "complete": 96, "incomplete": 24,
  "required_columns": ["Product name", "SKU", "Width"],
  "missing_by_column": { "width": {"label": "Width", "missing": 18} }
}
```

`list_items` with `incomplete_only: true` returns only the products that are not ready, each with a
`missing_required` list. That is the shape an assistant needs to be useful here: "these 24 items are
missing a width, and here they are."

A value of `0` counts as supplied. Only empty and whitespace-only values are treated as missing — a
required column with a legitimate zero is not a gap.

### Roles

| Role | Can |
|---|---|
| `viewer` | Read and export catalogs, items and completeness |
| `member` | …plus upload, **run extractions**, correct products, edit schemas |
| `admin` | …plus settings, branding, restore, the team |

An API key can do exactly what its owner can do. Because no tool starts a run, a member's key and a
viewer's key differ here only in what they may read and export.

## Spec sheet (PDF)

    GET /products/{id}/spec.pdf

One page per item: the normalized attributes, your logo and accent. Required attributes that are not
specified are **listed as not specified** rather than omitted, so the gap is visible to whoever reads
the sheet.

Since 3.2.0 the spec sheet uses the product's approved written title and description when it has
them.

## Catalog sheet (PDF)

    GET /catalogs/{id}/sheet.pdf

The reviewed products of a catalog in one PDF: a section per product (a confirmed variant group is
one section with a table of its variants: SKU, each option, price), the approved written description
or else the supplier's own, the specifications, and a closing line naming how many unreviewed
products were left out. Browser session; any role that can export.

## Marketplace feeds

    POST /catalogs/{id}/feed    (target=google|meta, link, brand, condition, availability)

A Google Merchant Center feed (tab-separated) or a Meta catalog feed (CSV) of the reviewed
products that have a price and an image URL. Browser session with its CSRF token; any role that can
export. The settings are remembered for the catalog when the role may edit it.

## Completeness export

    GET /catalogs/{id}/incomplete.csv

Every product missing a required attribute, with the list of what it is missing — the working file
for whoever is filling the gaps.

## Supplier lists on a schedule

    php bin/fetch-suppliers.php

Run from your own cron (once a day is enough). For each supplier profile whose schedule is due it
downloads the list from the profile's https:// address and files a new catalog only when the file
changed; extraction stays a click in the browser. There is no HTTP endpoint for it: a fetch reaches
out to another server, so it runs where you can see it.

## Scheduled backup

    GET /backup/run?t=<token>

Mint the token in **Backup & restore**. Fourteen files are kept and older ones pruned. Both JSON
backups — the scheduled one and the download — strip the same credentials: your AI provider key,
SMTP and OIDC secrets, and every password and API-key hash. The `.sqlite` download is the whole
install.


## Connect it to Zapier, Make or n8n

Specta talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.

### Before you start

- In Specta, open **API & Webhooks**. Mint an API key (choose **Read only** if the other tool only reads), and add a webhook when you have its URL (below).
- Use your install's full address, for example `https://specta.yourcompany.com`.

### When something happens in Specta: send it to another tool

1. In the other tool, create the trigger that gives you a URL to receive webhooks:
   - **Zapier:** the "Webhooks by Zapier" app, trigger **Catch Hook** (Webhooks by Zapier needs a paid Zapier plan).
   - **Make:** module **Webhooks → Custom webhook**.
   - **n8n:** the **Webhook** node.
2. Copy that URL into Specta under **API & Webhooks**, and choose the event or events below.
3. Make the event happen once in Specta, so the other tool receives a sample and you can map its fields.

| Event | Fires when |
|---|---|
| `batch.completed` | A batch leaves , whether it finished cleanly or with failures |
| `catalog.extracted` | A catalog has been read: a single upload's extraction, or each file a batch reads. The payload carries the catalog and its count |

Every delivery carries `X-Specta-Signature`, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.

### When something happens elsewhere: act in Specta

- **Zapier:** "Webhooks by Zapier", action **POST** (or **Custom Request**).
- **Make:** module **HTTP → Make a request**.
- **n8n:** the **HTTP Request** node.

Send the request to your install's address plus the path from the Endpoints table above, with the header `Authorization: Bearer <your API key>` and a JSON body. A read-only key cannot write.

The endpoints that change something:

- `POST /api/catalogs`: Queue a supplier document (multipart/form-data)

### Example recipes

- **batch.completed → a row in Google Sheets:** webhook trigger on `batch.completed` (A batch leaves , whether it finished cleanly or with failures), then the Google Sheets "Create row" action with the fields you want.
- **catalog.extracted → a message in Slack or Teams:** webhook trigger on `catalog.extracted` (A catalog has been read: a single upload's extraction, or each file a batch reads. The payload carries the catalog and its count), then the Slack or Microsoft Teams "Send message" action.
- **A new form entry or row elsewhere → Specta:** the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling `POST /api/catalogs` (Queue a supplier document (multipart/form-data)).

These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Specta's own automated tests check its API.
