Source: http://www.poma-ai.com/docs/grill/reference/api

# Grill API Reference

Reference for every `/grill/*` endpoint on **POMA AI API v3**.

- **Base URL:** `https://api.poma-ai.com/v3`
- **Auth:** `Authorization: Bearer <token>` for every endpoint. The token may be a **login token**, an **account API key**, or a **project API key** for a project with `product: "grill"`. When you authenticate with account-level credentials, add the **`X-Project-ID`** header to select the target project (protected projects reject account credentials — use a project API key).
- **Spec:** [Swagger UI](https://api.poma-ai.com/v3/docs)

For higher-level walkthroughs, see [Ingestion](/grill/concepts/ingestion), [Retrieval](/grill/concepts/retrieval), [RetrievalContext format](/grill/concepts/retrieval-context), and [Document management](/grill/concepts/document-management).

## Endpoint summary

| Method | Path | Purpose |
| --- | --- | --- |
| `POST`   | `/grill/ingest` | Submit a file for ingestion into the project's Grill namespace. |
| `POST`   | `/grill/ingestBucket` | Bulk-ingest a whole S3/GCS bucket (or prefix) as one task. |
| `POST`   | `/grill/search` | Hybrid search across the namespace; returns prompt-ready context. |
| `POST`   | `/grill/searchInDoc` | Same as `/grill/search` but `doc_filter` is required. |
| `GET`    | `/grill/attributes` | List the project's typed attributes (name + type) and the name budget. **Call before declaring a new attribute.** |
| `GET`    | `/grill/docs` | List documents in the namespace. |
| `GET`    | `/grill/docs/{docId}` | Get metadata for one document. |
| `DELETE` | `/grill/docs/{docId}` | Remove a document's vectors and storage. |

The standard `/jobs/{job_id}/...` endpoints (status, delete) are reused by Grill ingest jobs — they are documented under the main API.

---

## `POST /grill/ingest`

Create an ingestion job. Either send the file as **raw bytes** (`application/octet-stream`, filename carried in `Content-Disposition`) **or** point at a public URL with `X-Remote-URL` and let the server fetch it. Multipart is not accepted.

> **Request-size limit:** the HTTP body is capped at **50 MB** — larger uploads are rejected with `413` before processing starts. For files above that, host them and pass `X-Remote-URL` so the server fetches the bytes instead of receiving them in the request body.

**Request headers**

| Header | Required | Notes |
| --- | --- | --- |
| `Authorization` | yes | `Bearer <token>` — login token, account API key, or project API key (project must have `product: "grill"`). |
| `X-Project-ID` | conditional | Required only when authenticating with account-level credentials, to select the target project. Omit when using a project API key. |
| `Content-Type` | conditional | `application/octet-stream` for a raw-bytes upload. Not needed when using `X-Remote-URL`. |
| `Content-Disposition` | conditional | `attachment; filename="<name>.<ext>"`. The extension drives parser selection. Not required when using `X-Remote-URL`. |
| `X-Remote-URL` | no | A publicly accessible URL to fetch the file from instead of uploading bytes. When set, `Content-Disposition` and the request body are optional. |
| `X-Unencrypted-Strings` | no | JSON object of plaintext key→value metadata for **wildcard/glob** filtering. Stored **unencrypted** (vendor-visible — do **not** put secrets/PII here; use an `encrypted_text` attribute in `X-Attributes` for sensitive values). Max 32 keys; keys `[A-Za-z0-9_]` (case-insensitive) ≤ 64 chars, values ≤ 1024 chars, ≤ 16384 chars total. e.g. `{"project": "acme-merger", "dept": "legal"}`. Filter at search time with `unencrypted_strings_match`. |
| `X-Base-URL` | no | Base URL used to resolve relative image links in the input file. |
| `X-Completion` | no | URL (and optional headers) to send a completion webhook to when the job finishes. |
| `X-Attributes` | no | **Typed attributes** — one JSON object holding any type. **Before declaring a name, call [`GET /grill/attributes`](#get-grill-attributes)** and reuse an existing name and type where one fits. `string`/`int`/`float`/`bool` and their arrays are inferred; `datetime` and `encrypted_text` must be declared via `X-Attribute-Schema` (the reserved `labels` is typed `[]encrypted_text` automatically). e.g. `{"region":"emea","revision":12}`. A name's **type and the name itself are permanent** — the first document that uses a name fixes both, and each project has a budget of 64 names. Names must match `^[a-z0-9_]{1,64}$`. **On grill ingest this header is the only carrier** — grill ingest is octet-stream, with no multipart form path — so attributes are capped at 2048 chars per request. Declare `datetime` / `encrypted_text` with the companion `X-Attribute-Schema` header (same cap). Filter at search time with `attribute_filters`. |
| `X-Attribute-Schema` | no | Type declarations for entries in `X-Attributes`, shaped `{"name": {"type": "…"}}`. Required for `datetime` and `encrypted_text`, which cannot be inferred from a value. A declaration that conflicts with the type already in force for that name returns 400 `attribute_type_conflict`. |

> **Opaque vs. plaintext metadata.** An `encrypted_text` attribute in `X-Attributes` is HMAC'd — the vector DB never sees the values — so it is safe for sensitive values, at the cost of exact-token matching. `X-Unencrypted-Strings` is stored in plaintext (vendor-visible) precisely so its values can be **wildcard-matched** (filter via `unencrypted_strings_match`) — never put secrets or PII there.
>
> **Legacy fields.** `X-Labels`, `X-Meta-Int-1` and `X-Meta-Int-2` on ingest, and `meta_tags_any`, `meta_tags_all`, `meta_int_1_gte` / `_lte` and `meta_int_2_gte` / `_lte` on search, are still accepted and mapped onto typed attributes. Their removal will be announced in advance; see [Migrating from labels / meta_int](/grill/concepts/ingestion#migrating-from-labels-meta-int) for the replacements. PrimeCut is unaffected by this retirement and continues to accept `X-Labels`.

**Response (`201 Created`)** — `PublicJob`

```json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2026-04-30T10:00:00Z",
  "properties": {
    "file": { "filename": "manual.pdf", "size": 1048576 }
  }
}
```

**Status codes**

| Status | Meaning |
| --- | --- |
| `201` | Job created. Poll status with `GET /jobs/{job_id}/status` or stream via `GET /status/v1/jobs/{job_id}`. |
| `400` | Bad request (no `X-Remote-URL` **and** missing `Content-Disposition`, unsupported MIME, or empty body). |
| `401` | Missing/invalid Bearer token. |
| `403` | Multipart submitted, or the project's product is `primecut`. |
| `500` | Server error. |

See [Ingestion](/grill/concepts/ingestion) for the full lifecycle.

---

## `POST /grill/ingestBucket`

Bulk-ingest an entire S3 or GCS bucket (or a prefix inside one) as a single **task** — the server lists the objects and streams each file through the normal Grill pipeline, so nothing passes through your HTTP request body (the 50 MB request limit does not apply here).

Same auth as the rest of the `/grill/*` surface: project API key, or account credentials + `X-Project-ID`.

**Request body**

```json
{
  "source": {
    "type": "s3",                  // "s3" or "gcs"
    "bucket": "my-corpus",
    "prefix": "contracts/",        // optional
    "region": "eu-central-1",      // optional
    "endpoint": "…",               // optional (S3-compatible stores)
    "credentials": { "access_key_id": "…", "secret_access_key": "…" }
  },
  "filter_glob": "**/*.pdf",       // optional
  "dry_run": false,                // true = validate + sample listing, no task created
  "acknowledge_egress_cost": true
}
```

Credentials per source type: `s3` needs `access_key_id` + `secret_access_key`; `gcs` needs `service_account_json`. An optional `X-Completion` header (pipe-separated webhook/email targets, max 1024 bytes) fires once when the task reaches a terminal state.

**Response** — `202 Accepted`:

```json
{
  "bucket_ingest_task_id": "…",
  "state": "created",
  "state_description": "…",
  "poll_url": "/v3/grill/ingestBucket/{id}",
  "polling_guidance": { "min_interval_seconds": …, "max_interval_seconds": … }
}
```

With `dry_run: true` you instead get a validation report: a sample of discovered objects and, for cross-region setups, a rough egress-cost estimate.

**Task lifecycle & sub-operations** (all under `/grill/ingestBucket/{id}`):

| Operation | Effect |
| --- | --- |
| `GET /{id}` | Task status. States: `created` → `queued_org` → `listing` → `materialising` → `done` / `done_with_errors` / `failed`; may pause as `paused_credit_exhausted`, `paused_credential_error`, `paused_destination_credential_error`, `paused_egress_quota_exhausted`, `paused_user`. |
| `POST /{id}/pause` · `/{id}/resume` | Pause / resume the task. |
| `DELETE /{id}` | Cancel the task. |
| `GET /{id}/failures` | List per-file failures. |
| `POST /{id}/retryFailed` | Re-queue the failed files. |
| `PATCH /{id}/credentials` | Rotate source credentials (e.g. after `paused_credential_error`). |

**Limits**

| Limit | Value |
| --- | --- |
| Active tasks per account | **1** — a second submit returns `409` while one is running |
| Max size per file | **500 MB** |
| Max total size per task | **500 GB** |
| Files processed concurrently | 100 per account |
| Files per task | Capped during the phased rollout (target ceiling: 100,000). Listing beyond the cap ends the task as `done_with_errors` — everything discovered up to the cap is ingested. |

Documents land in the project namespace and are searchable exactly like single-file ingests.

---

## `POST /grill/search`

Hybrid retrieval. Returns a prompt-ready `RetrievalContext`.

**Request body** — `GrillSearchRequest`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | yes | Natural-language query. |
| `doc_filter` | string | no | Optional single-document id filter. Use `/grill/searchInDoc` when this is required. |
| `doc_ids` | array of string | no | Restrict search to this set of document ids. |
| `exclude_doc_ids` | array of string | no | Doc ids to exclude from results (max 100). Useful in agent loops to avoid re-citing docs already shown. |
| `attribute_filters` | array of `{attr, op, value}` | no | Filter on typed attributes. Operators depend on the attribute's type: `eq ne in nin glob iglob` (string); `eq ne in nin lt lte gt gte` (int); `eq ne lt lte gt gte` (float, datetime); `eq ne` (bool); `contains contains_any eq in` (`[]string`); `contains contains_any any_lt any_gt` (`[]int`, `[]float`); `contains any_lt any_gt` (`[]datetime`); `contains contains_any` (`[]bool`); `has_all_tokens has_any_tokens` (`encrypted_text`, `[]encrypted_text`). An operator the type does not offer returns 400 naming the ones it does. Clauses AND together. A filter naming an attribute the project has never used matches **nothing** — it is not ignored; list the names that exist with [`GET /grill/attributes`](#get-grill-attributes). `encrypted_text` matching is **exact**: same tokenization on both sides, no stemming, no partial words. Max 32 clauses. |
| `unencrypted_strings_match` | object (string→string) | no | Match on plaintext `X-Unencrypted-Strings`: a key→glob-pattern map. Case-insensitive Unix glob (`*` = any run, `?` = one char; a pattern with no wildcard is an exact match). Multiple keys AND together. |
| `return_assets` | boolean | no | Return cited docs' figures (and tables, where available) in an `assets` field keyed by `doc_id`; images are base64 data URIs, referenced as `[IMAGE: name]` in the context. |
| `format` | string | no | `prompt_ready` (default) renders the XML+Markdown context block; `json` returns structured ranked hits instead. |
| `min_relevance` | float `0..1` | no | Relevance floor (default `0.3`). Hits below it are dropped. Higher = stricter, lower = more permissive. |
| `min_top_relevance` | float `0..1` | no | Floor for the **top** hit. If the best hit scores below this, the entire result set comes back empty. |
| `target_tokens` | integer `100..500000` | no | Soft token budget — the typical answer size (default `5000`). Hits are admitted best-first up to this. **The knob to size a response.** Can never exceed `max_tokens` (clamped down if it would). |
| `max_tokens` | integer `100..500000` | no | Hard ceiling (default `15000`). Grill expands past `target_tokens` toward it only for tight multi-document clusters. Set it **below** `target_tokens` and the soft target is clamped down to match — the hard ceiling always wins; it never inflates a response on its own. |
| `expand_tightness` | float `0..1` | no | How aggressively the engine expands context around each hit. |
| `retrieval_tier` | string | no | Per-query retrieval tier — `standard` (fusion only) or `advanced` (adds a reranker). See [Retrieval tiers](/grill/concepts/retrieval-tiers). |
| `premium` | boolean | no | **Legacy** flag — `true` is equivalent to `retrieval_tier: "advanced"`. Prefer `retrieval_tier`. |

> Result count is bounded server-side by relevance and the token budget — there is **no `top_k`** parameter.

**Response (`200 OK`)** — `RetrievalContext` (default, `format: "prompt_ready"`)

```json
{
  "context": "<context><doc id=\"…\">…</doc></context>",
  "query": "How did operating margin change year over year?",
  "namespace": "project_docs_rag_4f2",
  "result_count": 4,
  "tokens_estimated": 5820,
  "results_dropped": 2,
  "detected_lang": "english",
  "mode": "advanced",
  "search_units": 1
}
```

`context` is the prompt-ready block (see [RetrievalContext format](/grill/concepts/retrieval-context) for the grammar) and the field you'll use 99% of the time; the siblings are metadata — `result_count` / `results_dropped` (how many hits made it / were dropped for budget), `tokens_estimated` (rendered size), `detected_lang`, `mode` (retrieval tier used), and `search_units` (billing). When `return_assets: true` and a cited doc has figures/tables, an `assets` object (keyed by `doc_id`) is also present; images are base64 data URIs referenced as `[IMAGE: name]` inside `context`.

With **`format: "json"`** the response is a `SearchResponse` instead: a `results` array of per-doc hits (`doc_id`, `content`, `title`, `canonical_url`, `pages`, `score`, `scores.{ann,bm25,rrf,reranker}`, …) plus the same `query` / `namespace` / `result_count` / `detected_lang` / `results_dropped` metadata — no rendered `context`.

**Status codes**

| Status | Meaning |
| --- | --- |
| `200` | OK. |
| `400` | Validation error or upstream Grill `400`. |
| `401` | Missing/invalid token, or upstream `401`. |
| `403` | Upstream Grill `403`. |
| `404` | `doc_filter` references an unknown doc. |
| `502` | Other upstream Grill / proxy error. |
| `503` | Cannot reach the Grill backend. |

---

## `POST /grill/searchInDoc`

Same shape as `/grill/search`, but **`doc_filter` is required and must be non-empty**. Use this when you want server-side enforcement that retrieval stays inside one document.

**Request body** — `GrillSearchInDocRequest`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | **yes** | Natural-language query. |
| `doc_filter` | string | **yes** | Document id to restrict search to. |
| `return_page_images` | boolean | no | **Deprecated / not available** — no-op today (page screenshots will be served via a dedicated endpoint, not inline). |

It also accepts every optional field from [`/grill/search`](#post-grill-search) — `doc_ids`, `exclude_doc_ids`, `attribute_filters`, `unencrypted_strings_match`, `return_assets`, `format`, `min_relevance`, `min_top_relevance`, `target_tokens`, `max_tokens`, `expand_tightness`, `retrieval_tier`, and `premium` — with the same semantics.

> Same as `/grill/search`: result count is bounded by relevance and the token budget; there is no `top_k`.

**Response (`200 OK`)** — `RetrievalContext` (identical to `/grill/search`).

**Status codes**

| Status | Meaning |
| --- | --- |
| `200` | OK. |
| `400` | Missing `query`, missing/empty `doc_filter`, or upstream `400`. |
| `401` | Missing/invalid token. |
| `403` | Upstream Grill `403`. |
| `404` | Doc not found in this project's namespace. |
| `502` | Upstream / proxy error. |
| `503` | Grill backend unreachable. |

---

## `GET /grill/attributes`

List the typed attributes already in use in the project: each name with its type, plus the project's name budget. No request body, no query parameters. Same auth as every other Grill route (add `X-Project-ID` with account-level credentials).

**Call this before declaring a new attribute.** A name and its type are permanent once the first document uses them, and every name counts against `max_names`. Agents and integrations should read this list first, reuse an existing name and type where one fits (don't invent `doc_year` when `year` already exists), and only introduce a new name when nothing fits. It is also the way to learn which names — and so which `attribute_filters` operators — exist before searching.

```bash
curl -sS "$GRILL/attributes" -H "authorization: Bearer $GRILL_KEY"
```

**Response (`200 OK`)**

```json
{
  "attributes": [
    { "name": "region", "type": "string" },
    { "name": "revision", "type": "int" },
    { "name": "published_at", "type": "datetime" },
    { "name": "case_notes", "type": "encrypted_text" }
  ],
  "max_names": 64,
  "note": "…"
}
```

- `type` is one of `string`, `int`, `float`, `bool`, `datetime`, `encrypted_text`, or an array form (`[]string`, `[]int`, `[]float`, `[]bool`, `[]datetime`, `[]encrypted_text`).
- `max_names` is the per-project cap on distinct names (64). Names already listed are spent; clearing a value does not free its name.
- `note` is human-readable guidance; don't parse it.
- An empty `attributes` array means the project has no typed attributes yet.

**Status codes**

| Status | Meaning |
| --- | --- |
| `200` | OK. |
| `401` | Missing/invalid token. |
| `403` | The key has no access to this project, or the project does not have the Grill product. |
| `410` | The project was purged (`namespace_purged`). |
| `503` | The attribute schema is temporarily unreadable. **Retry** with backoff — never treat a `503` as "no attributes exist", or you will declare duplicates of names that are already there. |

---

## `GET /grill/docs`

List documents in the project's namespace.

**Response (`200 OK`)** — `ListDocsResponse`

```json
{
  "namespace": "project_docs_rag_4f2",
  "total_documents": 3,
  "documents": [ DocInfo, … ]
}
```

| Field | Type | Description |
| --- | --- | --- |
| `namespace` | string | Project namespace identifier in the Grill backend. |
| `total_documents` | integer | Count of documents in the namespace. |
| `documents` | array | `DocInfo` entries (see below). |

**Status codes**

| Status | Meaning |
| --- | --- |
| `200` | OK. |
| `401` | Missing/invalid token. |
| `403` | Upstream Grill `403`. |
| `404` | Upstream Grill `404`. |
| `502` / `503` | Upstream issues. |

---

## `GET /grill/docs/{docId}`

Fetch metadata for one document.

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `docId` | string | The exact `doc_id` returned by `/grill/docs`. |

**Response (`200 OK`)** — `DocInfo`

| Field | Type | Description |
| --- | --- | --- |
| `doc_id` | string | Stable identifier within the namespace. |
| `title` | string | Detected title (filename fallback). |
| `language` | string | BCP-47 language code. |
| `filename` | string | Original ingest filename. |
| `canonical_url` | string | Citation URL stamped at ingest by Core (e.g. the resolved target of a short link / redirect when ingesting from a URL). Empty when not applicable. |
| `pages` | integer | Source page count. |
| `chunkset_count` | integer | Chunksets produced. |
| `chunk_count` | integer | Chunks produced. |
| `image_count` | integer | Detected figures/images. |
| `table_count` | integer | Detected tables. |
| `ingested_at` | string (RFC 3339) | Job completion time. |
| `source_job_id` | string | `job_id` of the originating `/grill/ingest`. |
| `bm25_state` | string | State of the document's BM25 (lexical) index. |

**Status codes**

| Status | Meaning |
| --- | --- |
| `200` | OK. |
| `400` | Empty `docId`. |
| `401` | Missing/invalid token. |
| `403` | Upstream Grill `403`. |
| `404` | Doc not found in this namespace. |
| `502` / `503` | Upstream issues. |

---

## `DELETE /grill/docs/{docId}`

Remove a document's vectors and storage from the namespace. Project, key, and other documents are unaffected.

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `docId` | string | Document to delete. |

**Response (`200 OK`)** — `DeleteDocResponse`

| Field | Type | Description |
| --- | --- | --- |
| `doc_id` | string | Echoes the deleted `docId`. |
| `vectors_deleted` | integer | Vector count removed from the namespace. |
| `storage_deleted` | boolean | Whether the document's stored content and assets were removed. (Page images are never stored by grill regardless of `storage_deleted`; nothing there was ever written.) |

**Status codes**

| Status | Meaning |
| --- | --- |
| `200` | OK. |
| `400` | Empty `docId`. |
| `401` | Missing/invalid token. |
| `403` | Upstream Grill `403`. |
| `404` | Doc not found in this namespace. |
| `502` / `503` | Upstream issues. |

---

## Schemas (canonical shapes)

These are the same definitions used by the OpenAPI v3 spec. All are JSON.

### `GrillSearchRequest`

```jsonc
{
  "query": "string (required)",
  "doc_filter": "string",         // restrict to one doc
  "doc_ids": ["string"],          // restrict to a set of docs
  "exclude_doc_ids": ["string"],  // max 100
  "attribute_filters": [{ "attr": "region", "op": "eq", "value": "emea" }], // typed attributes
  "unencrypted_strings_match": { "path": "legal/contracts/*" }, // glob on X-Unencrypted-Strings
  "return_assets": false,
  "return_page_images": false,    // deprecated, no-op
  "format": "prompt_ready",       // or "json" for structured hits
  "min_relevance": 0.3,           // relevance floor, 0..1 (default 0.3)
  "min_top_relevance": 0.0,       // floor for the top hit; below → empty set
  "target_tokens": 5000,          // soft budget / answer size (default 5000, 100..500000; ≤ max_tokens)
  "max_tokens": 15000,            // hard ceiling (default 15000, 100..500000)
  "expand_tightness": 0.0,        // context-expansion aggressiveness, 0..1
  "retrieval_tier": "standard",   // or "advanced" (reranker)
  "premium": false                // legacy: true == retrieval_tier "advanced"
}
```

> Every field except `query` is optional. `min_relevance` tunes precision (higher = stricter); `target_tokens` sizes the answer; `max_tokens` is a ceiling that only matters for tight multi-document clusters (set it below `target_tokens` and the soft target is clamped down to match — the hard ceiling always wins). `attribute_filters` and `unencrypted_strings_match` filter against metadata set at ingest (all combined via AND). `retrieval_tier` picks the reranking tier — see [Retrieval tiers](/grill/concepts/retrieval-tiers).

### `GrillSearchInDocRequest`

Same fields as `GrillSearchRequest`, except both `query` **and** `doc_filter` are required and `doc_filter` must be non-empty.

### `RetrievalContext`

```jsonc
{
  "context": "string (XML+Markdown — see RetrievalContext format page)",
  "query": "string",
  "namespace": "string",
  "result_count": 0,        // hits included in context
  "tokens_estimated": 0,    // rendered size of context
  "results_dropped": 0,     // hits dropped for the token budget
  "detected_lang": "string",// BM25 query language; null on low confidence
  "mode": "string",         // retrieval tier actually used
  "search_units": 0,        // billing units
  "assets": {               // present only when return_assets=true and assets exist
    "<doc_id>": { "images": [], "tables": [] }
  }
}
```

> `format: "json"` instead returns a `SearchResponse`: `{ results: [{ doc_id, content, title, canonical_url, pages, chunk_indices, score, scores: { ann, bm25, rrf, reranker } }], query, namespace, result_count, detected_lang, results_dropped }`.

### `DocInfo`

```jsonc
{
  "doc_id": "string (required)",
  "title": "string",
  "language": "string",
  "filename": "string",
  "canonical_url": "string",   // citation URL, set by Core at ingest
  "pages": 0,
  "chunkset_count": 0,
  "chunk_count": 0,
  "image_count": 0,
  "table_count": 0,
  "ingested_at": "RFC 3339 timestamp",
  "source_job_id": "uuid",
  "bm25_state": "string"
}
```

### `ListDocsResponse`

```jsonc
{
  "namespace": "string",
  "total_documents": 0,
  "documents": [ /* DocInfo */ ]
}
```

### `DeleteDocResponse`

```jsonc
{
  "doc_id": "string",
  "vectors_deleted": 0,
  "storage_deleted": true
}
```

### `PublicJob` (for `/grill/ingest`)

```jsonc
{
  "job_id": "uuid",
  "created_at": "RFC 3339 timestamp",
  "status": { "job_id": "…", "status": "pending|processing|done|failed", "code": 200 },
  "properties": { "file": { "filename": "string", "size": 0 }, "base_url": "string" }
}
```

## See also

- [Authentication](/grill/getting-started/authentication)
- [Quickstart](/grill/getting-started/quickstart)
- [RetrievalContext format](/grill/concepts/retrieval-context)
- [API v3 Swagger](https://api.poma-ai.com/v3/docs)