# Developer documentation

Developer documentation for the Perceptis API and MCP server.

Perceptis offers two ways to generate PowerPoint slides and decks programmatically:

* **Perceptis API v1** — REST API for server-side integrations, workflow automation, and internal tools that need `.pptx` output.
* **Perceptis MCP server** — Model Context Protocol surface for AI assistants (Claude, Cursor, etc.) to generate and edit slides directly from chat.

## Perceptis API v1

Start here:

* [**Overview**](/perceptis-api-v1/public-api) - how generation jobs work.
* [**Authentication**](/perceptis-api-v1/authentication) - create an API key and send it with requests.
* [**Quickstart: single slide**](/perceptis-api-v1/quickstart-single-slide) - generate one slide and poll for completion.
* [**Quickstart: complete deck**](/perceptis-api-v1/quickstart-deck) - generate a complete presentation deck.

Then use these pages as needed:

* [**Templates**](/perceptis-api-v1/templates) - choose a template by name or use automatic template selection.
* [**Full client script example**](/perceptis-api-v1/examples) - run the included generate-and-poll script.
* [**Errors, rate limits, and billing**](/perceptis-api-v1/errors-and-limits) - handle common API responses.
* [**API reference**](/api-reference/reference) - endpoint summaries and request shapes.

## Perceptis MCP Server

Use Perceptis directly from any MCP-compatible AI client. The fastest path is connecting from claude.ai's web UI — no API key required.

* [**Overview**](/perceptis-mcp-server/mcp) - what the MCP server exposes and how it differs from the REST API.
* [**Authentication**](/perceptis-mcp-server/authentication) - SSO sign-in for claude.ai, or reuse your API key for CLI/desktop clients.
* [**Quickstart: single slide**](/perceptis-mcp-server/quickstart-slide) - connect a client and generate your first slide.
* [**Quickstart: complete deck**](/perceptis-mcp-server/quickstart-deck) - generate a multi-slide deck.
* [**Tools reference**](/perceptis-mcp-server/tools) - every tool the server exposes, with costs and parameters.
* [**Credits and limits**](/perceptis-mcp-server/credits-and-limits) - credit costs, rate limits, and download link expiry.
* [**Error codes**](/perceptis-mcp-server/errors) - the full list of codes the tools return.


# Overview

Overview of Perceptis API v1.

The Perceptis API lets you generate PowerPoint slides and decks from your own systems. Submit a prompt, choose an output type and optional template, poll for completion, then download the generated `.pptx` file.

## Overview

Generation is asynchronous:

1. Send `POST /api/v1/generate` with a prompt and `output_type`.
2. Receive a `job_id`.
3. Poll `GET /api/v1/status/{job_id}` with the same API key.
4. When `status` is `completed`, download the PowerPoint file from the `downloads` array.

The API supports:

* Single-slide generation with optional variants.
* Complete-deck generation.
* Template selection by name.
* Optional web search and knowledge base context.
* Reference images for single-slide generation.
* Idempotent generate requests for safe retries.
* Status polling with `Retry-After` guidance.

## Who can use it

API access is available only on paid Business and Enterprise plans. Free, Starter, and trial organizations cannot create or use API keys.

Create and revoke API keys in **Developer Settings** in the Perceptis web app. Usage draws from your organization's credit pool.

## Base URL

Use the API origin as your base URL:

```
https://app.perceptis.ai
```

All v1 examples use paths under:

```
{PERCEPTIS_API_BASE_URL}/api/v1/
```

Set `PERCEPTIS_API_BASE_URL` to the origin only, without a path. For example, use `https://app.perceptis.ai`, not `https://app.perceptis.ai/api`.

See [Base URLs and environment](/perceptis-api-v1/infra-routing-and-env) for environment variable guidance.

## Polling

Poll every few seconds, set an integration-level timeout, and respect `429` responses with the `Retry-After` header. Completed status responses include short-lived download URLs; poll status again for fresh links whenever needed. Job downloads are visible only to the API key that created the job.

## Next steps

* [Authentication](/perceptis-api-v1/authentication)
* [Quickstart: single slide](/perceptis-api-v1/quickstart-single-slide)
* [Quickstart: complete deck](/perceptis-api-v1/quickstart-deck)
* [API reference](/api-reference/reference)


# Authentication

Perceptis API v1 authentication with API keys.

The Perceptis API uses bearer API keys. Create a key in **Developer Settings** in the Perceptis web app, store it securely, and send it on every API request.

Perceptis API access requires a paid Business or Enterprise organization plan. Free, Starter, and trial organizations are not eligible.

## Create an API key

1. Open **Developer Settings** in Perceptis.
2. Create a new API key.
3. Copy the plaintext key when it is shown.
4. Store it in a secret manager or environment variable.

The plaintext key is shown only once. If you lose it, revoke the old key and create a new one.

Keys look similar to:

```
sk-live-per-<random-base64url>
```

Treat the full key as opaque. Do not parse, log, or commit it.

## Authorization header

Send the key as a bearer token:

```http
Authorization: Bearer <your-api-key>
Content-Type: application/json
```

`Content-Type: application/json` is required for `POST /api/v1/generate`.

## Key behavior

* Use the same API key to poll a job that created it.
* Revoked or invalid keys receive `401`.
* If your organization is not eligible for API access, including after a downgrade or trial change, requests receive `403`.
* API usage draws from your organization's credit pool.

## Security practices

* Store keys in a secret manager or environment variable.
* Use separate keys for separate integrations when possible.
* Revoke keys that are no longer needed.
* Rotate keys if you suspect exposure.


# Quickstart: single slide

Quickstart for single-slide generation with safe polling.

This quickstart starts a single-slide generation job, polls until it finishes, and reads the download URL from the completed status response.

## Set environment variables

Set `PERCEPTIS_API_BASE_URL` to the API origin only. Do not include `/api`; the examples append `/api/v1/...`.

```bash
export PERCEPTIS_API_BASE_URL="https://app.perceptis.ai"
export PERCEPTIS_API_KEY="sk-live-per-..."
```

Install the Python HTTP client used by this quickstart:

```bash
python3 -m pip install requests
```

## Start generation

Use this as `quickstart_single_slide.py`:

```python
import os
import time
import uuid

import requests


base_url = os.environ["PERCEPTIS_API_BASE_URL"].rstrip("/")
api_key = os.environ["PERCEPTIS_API_KEY"]

response = requests.post(
    f"{base_url}/api/v1/generate",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "prompt": "One slide summarizing Q3 revenue drivers",
        "output_type": "single_slide",
    },
    timeout=60,
)
response.raise_for_status()

job_id = response.json()["job_id"]
print(f"Job ID: {job_id}")


def poll_status(job_id, timeout_sec=300):
    deadline = time.monotonic() + timeout_sec

    while time.monotonic() < deadline:
        status_response = requests.get(
            f"{base_url}/api/v1/status/{job_id}",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=60,
        )
        status_response.raise_for_status()

        body = status_response.json()
        if body["status"] in {"completed", "failed"}:
            return body

        time.sleep(3)

    raise TimeoutError(f"Job did not finish within {timeout_sec} seconds")


final = poll_status(job_id)

if final["status"] == "failed":
    raise RuntimeError(final.get("error") or "Generation failed")

for download in final.get("downloads") or []:
    print(download["url"])
```

The response includes a `job_id`:

```json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending",
  "output_type": "single_slide",
  "downloads": null,
  "error": null
}
```

## Poll status

Poll `GET /api/v1/status/{job_id}` with the same API key that created the job. The example above checks every 3 seconds and stops after 5 minutes. For retries, backoff, and `Retry-After` handling, see the [full client script example](/perceptis-api-v1/examples).

## Downloads

When `status` is `completed`, each item in `downloads` includes a short-lived `url` for a `.pptx` file. If a URL expires, poll status again for fresh links while the generated files remain available. Use the same API key that created the job.

Example completed response:

```json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "output_type": "single_slide",
  "downloads": [
    {
      "url": "https://app.perceptis.ai/downloads/550e8400-e29b-41d4-a716-446655440000.pptx?expires=3600&signature=...",
      "format": "pptx",
      "variant": 1
    }
  ],
  "error": null
}
```

## Optional: reference images

For single-slide generation, include `reference_images` when you want the prompt to use a visual reference:

```json
"reference_images": [
  {
    "data": "<base64>",
    "mime_type": "image/png"
  }
]
```

You can also include `variant_count`, `template_name`, `use_web_search`, or `use_knowledge_base` when needed. See the [`POST /api/v1/generate` reference](/api-reference/post-generate) for the full request body.


# Quickstart: complete deck

Quickstart for complete-deck generation with safe polling.

This quickstart starts a complete-deck generation job and polls until the `.pptx` deck is ready to download.

## Set environment variables

Set `PERCEPTIS_API_BASE_URL` to the API origin only. Do not include `/api`; the examples append `/api/v1/...`.

```bash
export PERCEPTIS_API_BASE_URL="https://app.perceptis.ai"
export PERCEPTIS_API_KEY="sk-live-per-..."
```

Install the Python HTTP client used by this quickstart:

```bash
python3 -m pip install requests
```

## Start generation

Use this as `quickstart_deck.py`:

```python
import os
import time
import uuid

import requests


base_url = os.environ["PERCEPTIS_API_BASE_URL"].rstrip("/")
api_key = os.environ["PERCEPTIS_API_KEY"]

response = requests.post(
    f"{base_url}/api/v1/generate",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "prompt": "Ten-slide board deck on market entry for EU expansion",
        "output_type": "deck",
    },
    timeout=60,
)
response.raise_for_status()

job_id = response.json()["job_id"]
print(f"Job ID: {job_id}")


def poll_status(job_id, timeout_sec=300):
    deadline = time.monotonic() + timeout_sec

    while time.monotonic() < deadline:
        status_response = requests.get(
            f"{base_url}/api/v1/status/{job_id}",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=60,
        )
        status_response.raise_for_status()

        body = status_response.json()
        if body["status"] in {"completed", "failed"}:
            return body

        time.sleep(3)

    raise TimeoutError(f"Job did not finish within {timeout_sec} seconds")


final = poll_status(job_id)

if final["status"] == "failed":
    raise RuntimeError(final.get("error") or "Generation failed")

for download in final.get("downloads") or []:
    print(download["url"])
```

The response includes a `job_id`:

```json
{
  "job_id": "3b4f1f54-66a9-4f5e-a681-60b1c55a8c9d",
  "status": "pending",
  "output_type": "deck",
  "downloads": null,
  "error": null
}
```

## Poll status

Poll `GET /api/v1/status/{job_id}` with the same API key that created the job. The example above checks every 3 seconds and stops after 5 minutes. For retries, backoff, and `Retry-After` handling, see the [full client script example](/perceptis-api-v1/examples).

## Completion

When `status` is `completed`, the deck appears in `downloads`:

```json
{
  "job_id": "3b4f1f54-66a9-4f5e-a681-60b1c55a8c9d",
  "status": "completed",
  "output_type": "deck",
  "downloads": [
    {
      "url": "https://app.perceptis.ai/downloads/3b4f1f54-66a9-4f5e-a681-60b1c55a8c9d.pptx?expires=3600&signature=...",
      "format": "pptx",
      "slide_count": 10
    }
  ],
  "error": null
}
```

If a URL expires, poll status again for fresh links while the generated files remain available. Use the same API key that created the job.

## Deck-specific fields

Use `output_type: "deck"` for complete-deck generation. Do not send `variant_count` or `reference_images` with deck requests.

You can also include `template_name`, `use_web_search`, or `use_knowledge_base` when needed. See the [`POST /api/v1/generate` reference](/api-reference/post-generate) for the full request body.


# Templates

Template selection by name and automatic defaults.

Use `template_name` to generate with a template your organization can access. If you omit it, Perceptis chooses the same default template experience used in the app.

## Choose a template

In `POST /api/v1/generate` you may set:

```json
"template_name": "Acme Executive"
```

Template names match the templates available to your organization in Perceptis. Use the same display name your team uses in the product.

## Use automatic template selection

If you omit `template_name`, Perceptis uses your organization's default template when one is configured. If no organization default is available, Perceptis uses another template available to your organization. If no template is available, generation still runs with a plain PowerPoint output.

## Ambiguous names

If a name matches more than one accessible template, the API returns `409` with error code `ambiguous_template_name`. Rename one of the templates or choose a unique template name before retrying.

## Practical tips

* Keep template names stable for production integrations.
* Prefer a small set of approved templates for automated generation.
* Treat template names as case-sensitive unless your Perceptis team tells you otherwise.


# Full client script example

Full runnable Perceptis API v1 client script.

Use the Python script on this page to start a generation job, poll until it finishes, and print the PowerPoint download URLs. It works for single-slide generation, deck generation, or both in one run.

The script handles the details that production callers should care about:

* It sends `POST /api/v1/generate` with an `Idempotency-Key`.
* It polls `GET /api/v1/status/{job_id}` with backoff.
* It honors `Retry-After` when the API asks the client to slow down.
* It prints timestamps, job IDs, status changes, and final download URLs.
* It leaves web search and knowledge base use off unless you enable them.

## Environment

Set the API origin and your API key:

```bash
export PERCEPTIS_API_BASE_URL="https://app.perceptis.ai"
export PERCEPTIS_API_KEY="sk-live-per-..."
```

`PERCEPTIS_API_BASE_URL` should be the origin only. Do not include `/api/v1`; the script adds the API paths.

## Run The Script

Generate both a single slide and a deck:

```bash
python3 generate_and_poll.py --mode both
```

Generate only a single slide:

```bash
python3 generate_and_poll.py \
  --mode single_slide \
  --single-slide-prompt "Generate a revenue stream comparison slide for SpaceX focusing on low earth orbit satellites versus deep space exploration."
```

Generate only a deck:

```bash
python3 generate_and_poll.py \
  --mode deck \
  --deck-prompt "Generate a consulting proposal deck for reshaping SpaceX from low earth orbit satellite launches toward deep space exploration."
```

Generate a single slide from a local reference image:

```bash
python3 generate_and_poll.py \
  --mode single_slide \
  --reference-image "/path/to/reference.png" \
  --single-slide-prompt "Recreate the provided reference image as a polished consulting slide. Match the layout, visual hierarchy, colors, labels, and composition as closely as possible."
```

Enable web search or knowledge base context only when you want those sources included:

```bash
python3 generate_and_poll.py \
  --mode deck \
  --use-web-search \
  --use-knowledge-base
```

## Safe retries

`--idempotency-key` and `job_id` have different roles:

* `--idempotency-key` is sent when creating a job. Reuse it with the same request body if you need to retry after a timeout or network error; Perceptis returns the original job instead of creating a duplicate.
* `job_id` is returned by Perceptis after the job is accepted. Use it to poll status and refresh download links.

If you omit `--idempotency-key`, the script generates one for the run. When `--mode both` is used, it derives separate keys for the single-slide and deck jobs.

Completed jobs can be polled again for fresh download links while the generated files remain available. Use the same API key that created the job.

## Full reference script

Use this as `generate_and_poll.py`:

```python
#!/usr/bin/env python3
"""Run Perceptis API single-slide and deck examples with safe status polling."""

from __future__ import annotations

import argparse
import base64
import json
import mimetypes
import os
import sys
import time
import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


TERMINAL_STATUSES = {"completed", "failed"}


@dataclass
class ApiResponse:
    status_code: int
    headers: Any
    body: dict[str, Any]


class PerceptisApiError(RuntimeError):
    def __init__(self, message: str, *, status_code: int | None = None) -> None:
        super().__init__(message)
        self.status_code = status_code


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Generate Perceptis API single-slide and deck jobs, poll status, and print downloads.",
    )
    parser.add_argument(
        "--base-url",
        default=os.environ.get("PERCEPTIS_API_BASE_URL"),
        help=(
            "API origin only (scheme://host[:port]); script appends /api/v1/.... "
            "Trailing /api is stripped if present."
        ),
    )
    parser.add_argument(
        "--api-key",
        default=os.environ.get("PERCEPTIS_API_KEY"),
        help="Perceptis API key. Defaults to PERCEPTIS_API_KEY.",
    )
    parser.add_argument(
        "--mode",
        choices=("single_slide", "deck", "both"),
        default="both",
        help="Which example request to run.",
    )
    parser.add_argument(
        "--single-slide-prompt",
        default="One slide summarizing Q3 revenue drivers.",
        help="Prompt used for the single-slide example.",
    )
    parser.add_argument(
        "--deck-prompt",
        default="A concise 4-slide board deck on market entry for EU expansion.",
        help="Prompt used for the deck example.",
    )
    parser.add_argument(
        "--template-name",
        help="Optional template name to include in generate requests.",
    )
    parser.add_argument(
        "--use-web-search",
        action="store_true",
        help="Enable web search for generation. Defaults to false.",
    )
    parser.add_argument(
        "--use-knowledge-base",
        action="store_true",
        help="Enable your organization's knowledge base for generation. Defaults to false.",
    )
    parser.add_argument(
        "--reference-image",
        action="append",
        default=[],
        help=(
            "Path to a reference image for single-slide generation. "
            "Repeat up to 3 times. Ignored unless --mode includes single_slide."
        ),
    )
    parser.add_argument(
        "--idempotency-key",
        help="Optional idempotency key. Defaults to a generated UUID per run.",
    )
    parser.add_argument(
        "--timeout-seconds",
        type=float,
        default=900.0,
        help="Maximum time to poll each job.",
    )
    parser.add_argument(
        "--initial-delay-seconds",
        type=float,
        default=3.0,
        help="Initial poll delay. Keep this in the 2-5 second range for normal use.",
    )
    return parser.parse_args()


def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def log(message: str) -> None:
    print(f"[{timestamp()}] {message}")


def reference_image_payload(path: str) -> dict[str, str]:
    mime_type, _ = mimetypes.guess_type(path)
    if mime_type not in {
        "image/png",
        "image/jpeg",
        "image/webp",
        "image/heic",
        "image/heif",
    }:
        raise PerceptisApiError(
            f"Unsupported reference image type for {path!r}. "
            "Use PNG, JPEG, WebP, HEIC, or HEIF."
        )

    with open(path, "rb") as image_file:
        data = base64.b64encode(image_file.read()).decode("ascii")

    return {"data": data, "mime_type": mime_type}


def build_reference_images(paths: list[str]) -> list[dict[str, str]]:
    if len(paths) > 10:
        raise PerceptisApiError("At most 10 --reference-image values are supported.")
    return [reference_image_payload(path) for path in paths]


def request_json(
    method: str,
    url: str,
    api_key: str,
    *,
    body: dict[str, Any] | None = None,
    idempotency_key: str | None = None,
    allow_statuses: set[int] | None = None,
) -> ApiResponse:
    payload = None
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Accept": "application/json",
    }
    if body is not None:
        payload = json.dumps(body).encode("utf-8")
        headers["Content-Type"] = "application/json"
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key

    request = Request(url, data=payload, headers=headers, method=method)
    try:
        with urlopen(request, timeout=60) as response:
            raw_body = response.read().decode("utf-8")
            return ApiResponse(
                status_code=response.status,
                headers=response.headers,
                body=json.loads(raw_body) if raw_body else {},
            )
    except HTTPError as exc:
        raw_body = exc.read().decode("utf-8")
        try:
            error_body = json.loads(raw_body) if raw_body else {}
        except json.JSONDecodeError:
            error_body = {"error": {"message": raw_body}}
        if allow_statuses and exc.code in allow_statuses:
            return ApiResponse(
                status_code=exc.code,
                headers=exc.headers,
                body=error_body,
            )
        retry_after = exc.headers.get("Retry-After")
        suffix = f" Retry-After={retry_after!r}." if retry_after else ""
        raise PerceptisApiError(
            f"{method} {url} failed with HTTP {exc.code}: {error_body}.{suffix}",
            status_code=exc.code,
        ) from exc
    except URLError as exc:
        raise PerceptisApiError(f"{method} {url} failed: {exc.reason}") from exc


def generate(
    base_url: str,
    api_key: str,
    *,
    output_type: str,
    prompt: str,
    template_name: str | None,
    idempotency_key: str,
    use_web_search: bool,
    use_knowledge_base: bool,
    reference_images: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
    body: dict[str, Any] = {
        "prompt": prompt,
        "output_type": output_type,
        "use_web_search": use_web_search,
        "use_knowledge_base": use_knowledge_base,
    }
    if output_type == "single_slide":
        body["variant_count"] = 1
        if reference_images:
            body["reference_images"] = reference_images
    if template_name:
        body["template_name"] = template_name

    response = request_json(
        "POST",
        f"{base_url}/api/v1/generate",
        api_key,
        body=body,
        idempotency_key=idempotency_key,
    )
    return response.body


def retry_after_seconds(value: str | None, default: float) -> float:
    if value is None:
        return default
    try:
        return max(float(value), 0.0)
    except ValueError:
        return default


def poll_status(
    base_url: str,
    api_key: str,
    job_id: str,
    *,
    timeout_seconds: float,
    initial_delay_seconds: float,
) -> dict[str, Any]:
    deadline = time.monotonic() + timeout_seconds
    delay = initial_delay_seconds

    while time.monotonic() < deadline:
        response = request_json(
            "GET",
            f"{base_url}/api/v1/status/{job_id}",
            api_key,
            allow_statuses={429},
        )

        if response.status_code == 429:
            delay = retry_after_seconds(response.headers.get("Retry-After"), delay)
            log(f"job {job_id}: rate limited, sleeping {delay:.1f}s")
            time.sleep(delay)
            delay = min(delay * 1.5, 30.0)
            continue

        status = response.body.get("status")
        log(f"job {job_id}: status={status}")
        if status in TERMINAL_STATUSES:
            return response.body

        time.sleep(delay)
        delay = min(delay * 1.3, 20.0)

    raise TimeoutError(f"Job {job_id} did not finish within {timeout_seconds} seconds")


def print_downloads(result: dict[str, Any]) -> None:
    if result.get("status") == "failed":
        print(f"job failed: {json.dumps(result.get('error'), sort_keys=True)}")
        return

    downloads = result.get("downloads") or []
    if not downloads:
        print("completed job did not include downloads")
        return

    print("download URLs:")
    for index, entry in enumerate(downloads, start=1):
        label = f"variant {entry['variant']}" if "variant" in entry else "deck"
        slide_count = entry.get("slide_count")
        if slide_count is not None:
            label = f"{label}, {slide_count} slides"
        print(f"- {index}. {label}: {entry['url']}")


def idempotency_key_for(base_key: str, output_type: str, run_count: int) -> str:
    if run_count == 1:
        return base_key
    return f"{base_key}-{output_type}"


def main() -> int:
    args = parse_args()
    if not args.base_url:
        print("Missing --base-url or PERCEPTIS_API_BASE_URL", file=sys.stderr)
        return 2
    if not args.api_key:
        print("Missing --api-key or PERCEPTIS_API_KEY", file=sys.stderr)
        return 2
    if args.reference_image and args.mode == "deck":
        print("--reference-image is only supported for single_slide generation", file=sys.stderr)
        return 2

    base_url = args.base_url.rstrip("/")
    if base_url.endswith("/api"):
        base_url = base_url[: -len("/api")].rstrip("/")
    modes = ["single_slide", "deck"] if args.mode == "both" else [args.mode]
    base_idempotency_key = args.idempotency_key or str(uuid.uuid4())
    reference_images = build_reference_images(args.reference_image)

    for output_type in modes:
        prompt = (
            args.single_slide_prompt
            if output_type == "single_slide"
            else args.deck_prompt
        )
        idempotency_key = idempotency_key_for(
            base_idempotency_key,
            output_type,
            len(modes),
        )
        print(f"starting {output_type} job with Idempotency-Key={idempotency_key}")
        job = generate(
            base_url,
            args.api_key,
            output_type=output_type,
            prompt=prompt,
            template_name=args.template_name,
            idempotency_key=idempotency_key,
            use_web_search=args.use_web_search,
            use_knowledge_base=args.use_knowledge_base,
            reference_images=reference_images if output_type == "single_slide" else None,
        )
        job_id = job["job_id"]
        log(f"accepted {output_type} job_id={job_id}")
        final = poll_status(
            base_url,
            args.api_key,
            job_id,
            timeout_seconds=args.timeout_seconds,
            initial_delay_seconds=args.initial_delay_seconds,
        )
        print_downloads(final)

    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except PerceptisApiError as exc:
        code = f" (HTTP {exc.status_code})" if exc.status_code is not None else ""
        print(f"Error: {exc}{code}", file=sys.stderr)
        raise SystemExit(1)
    except TimeoutError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        raise SystemExit(1)
```


# Errors, rate limits, and billing

Errors, rate limits, and billing for Perceptis API v1.

## Error response shape

Synchronous errors typically look like:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Human-readable detail"
  }
}
```

Branch on `error.code`, not on `message`. Messages are intended for debugging and may change.

## Common error codes

| Code                      | Meaning                                                                               |
| ------------------------- | ------------------------------------------------------------------------------------- |
| `invalid_request`         | The request is malformed, fails validation, or uses an unsupported field combination. |
| `not_found`               | The requested job or resource was not found for the current API key.                  |
| `ambiguous_template_name` | `template_name` matches more than one accessible template.                            |
| `idempotency_conflict`    | The same `Idempotency-Key` was reused with a different request body.                  |
| `rate_limit_exceeded`     | The integration is sending requests too quickly. Use `Retry-After` and back off.      |

Generation failures are returned by the status endpoint as `status: "failed"` with an `error` object.

## Rate limits

Perceptis applies limits to generation requests and status polling. If you receive `429`, read the `Retry-After` response header, wait at least that long, and retry with backoff.

Recommended polling behavior:

* Start with a 2 to 5 second interval.
* Increase the interval after repeated in-progress responses.
* Set a timeout in your integration.
* Do not poll more than once per second for the same job.

Repeated status polls for a completed job do not create additional generation charges.

## Billing and credits

API usage draws from your organization's Perceptis credit pool.

* Single-slide jobs are based on delivered variants.
* Deck jobs are based on delivered slides.
* Failed jobs do not consume credits.
* Polling status does not consume credits.

## Idempotency

Send an `Idempotency-Key` header on `POST /api/v1/generate` when retrying a request after a network error or timeout. Reuse the same key only with the same request body.

The idempotency key is not the job identifier. After the request is accepted, use the returned `job_id` to poll status and refresh download links.

If the same key is reused with a different body, the API returns `409` with `idempotency_conflict`.


# Base URLs and environment

Base URLs and environment variables for the Perceptis API.

## API base URL

Use the production API origin as your base URL:

```
https://app.perceptis.ai
```

All v1 paths are under:

```
{PERCEPTIS_API_BASE_URL}/api/v1/
```

For example:

* `POST https://app.perceptis.ai/api/v1/generate`
* `GET https://app.perceptis.ai/api/v1/status/{job_id}`

Do not include `/api` in `PERCEPTIS_API_BASE_URL`; client code and examples append `/api/v1/...`.

## Environment variables

| Variable                 | Use                                                         |
| ------------------------ | ----------------------------------------------------------- |
| `PERCEPTIS_API_BASE_URL` | API origin only, for example `https://app.perceptis.ai`.    |
| `PERCEPTIS_API_KEY`      | Your API key from Developer Settings. Store it as a secret. |

Copy [tooling-env.example](https://github.com/Whiteboard-Intelligence/perceptis/blob/main/docs/public/developer/public-api/tooling-env.example) as a starting point.

## Example

```bash
export PERCEPTIS_API_BASE_URL="https://app.perceptis.ai"
export PERCEPTIS_API_KEY="sk-live-per-..."
```

Then call:

```bash
curl -sS "${PERCEPTIS_API_BASE_URL}/api/v1/status/${JOB_ID}" \
  -H "Authorization: Bearer ${PERCEPTIS_API_KEY}"
```

## Organization-specific API hosts

If Perceptis provides a dedicated API host for your organization, use that origin in `PERCEPTIS_API_BASE_URL` and keep the same `/api/v1/...` paths.


# Versioning and changes

Versioning and change guidance for Perceptis API v1.

The Perceptis API uses versioned paths. The current generation API is under:

```
/api/v1/
```

Use the versioned paths shown in this documentation for production integrations.

## Non-breaking changes

Perceptis may add optional fields, response metadata, or additional error codes within `v1`. Build clients to ignore unknown fields they do not need.

## Breaking changes

Breaking changes are introduced with a new API version. Check this page when planning upgrades or reviewing integration behavior.

## Changes

### 2026-09 — Edit a PowerPoint file you already have

`POST /api/v1/edit` accepts a `.pptx` and a prompt, and returns an edited deck. It is a multipart request, because the deck is part of it. The job it creates polls and downloads exactly like a deck job from `POST /generate`.

What changes for your integration: nothing. This is a new endpoint; existing calls are unaffected.

See [`POST /api/v1/edit`](https://github.com/Whiteboard-Intelligence/perceptis/tree/main/docs/public/developer/public-api/reference/post-edit.md).

### 2026-08 — Deck jobs may complete with fewer slides than requested

A deck job now completes when at least one slide is produced. Before this change, a deck job failed if any slide failed.

What changes for your integration:

* `status` is `completed` when the deck holds one or more slides. The downloaded `.pptx` contains the slides that were produced.
* `downloads[0].slide_count` is the number of slides in the file. Read the slide count from this field, not from your prompt.
* `status` is `failed` only when no slide was produced. A failed job is not charged.
* You are charged for the slides in the file, and for no others.

This change is additive: no field is added, removed or renamed, and no existing field changes type. A client that reads `slide_count` needs no change.


# Overview

Use Perceptis directly from AI assistants via the Model Context Protocol.

The Perceptis MCP server lets MCP-compatible AI clients — including Claude.ai (web), Claude Desktop, Claude Code, and Cursor — generate and edit PowerPoint slides and decks using natural language.

**Server URL:** `https://app.perceptis.ai/mcp/v1`

The fastest way to connect is directly from the **claude.ai web UI** — add the URL above as a custom integration and sign in via SSO. No API key or config file needed. See the [quickstart](/perceptis-mcp-server/quickstart-slide) for all connection options.

Generation takes a few minutes. The AI client polls automatically — you just wait for the result.

## What you can do

| Say something like                         | What happens                | Cost               |
| ------------------------------------------ | --------------------------- | ------------------ |
| "Make a slide about Q3 results"            | Generates a single slide    | 1 credit           |
| "Change the title to Revenue Growth"       | Edits the slide             | Free               |
| "Create a 10-slide deck about our product" | Generates a full deck       | 1 credit per slide |
| "Update slide 3 to focus on EMEA"          | Edits one slide in the deck | Free               |


# Authentication

Create an API key and authenticate with the Perceptis MCP server.

The Perceptis MCP server supports two authentication methods depending on your client.

## Option 1: SSO (claude.ai web UI)

When connecting through the claude.ai web UI, no API key is needed. Go to [claude.ai/customize/connectors](https://claude.ai/customize/connectors), add `https://app.perceptis.ai/mcp/v1`, and sign in with **Google** or **Microsoft** when prompted. If you do not already have a Perceptis account, one is created automatically on the free tier so you can start using the MCP tools immediately.

See the [quickstart](/perceptis-mcp-server/quickstart-slide) for the full step-by-step flow.

## Option 2: API key (CLI, desktop, and headless clients)

For Claude Desktop, Cursor, Claude Code CLI, and other clients, authenticate with a bearer API key.

## Create an API key

1. Open [Developer Settings](https://app.perceptis.ai/developer-settings) in Perceptis.
2. Create a new API key.
3. Copy the plaintext key — it is shown only once.

Keys look like:

```
sk-live-per-<random>
```

Store the key in a secret manager or environment variable. Do not commit it.

## Pass the key

Send the key as a bearer token in every request:

```
Authorization: Bearer sk-live-per-YOUR_KEY
```

Revoked or invalid keys receive `401`.


# Quickstart: single slide

Connect your AI client and generate your first slide.

Two connection paths are supported. Pick the one that matches your client.

## Path A — Claude.ai web UI (recommended, no API key)

The easiest way to get started. Authentication goes through SSO — no API key, no config file.

1. Open [claude.ai/customize/connectors](https://claude.ai/customize/connectors).
2. Click **Add custom connector**.
3. Enter the server URL: `https://app.perceptis.ai/mcp/v1`
4. Claude redirects you through Perceptis SSO. Sign in with Google or Microsoft. If this is your first time, a free-tier Perceptis account is created automatically.
5. Once connected, the Perceptis tools appear in your Claude conversations.

Skip to [step 2: generate a slide](#2-generate-a-slide).

## Path B — API-key clients (Claude Code, Claude Desktop, Cursor)

For local CLI and desktop clients, authenticate with a bearer API key.

### 1. Get your API key

Go to [Developer Settings](https://app.perceptis.ai/developer-settings) and create a new API key. Keys look like `sk-live-per-<random>` and are shown only once — store the value before closing the dialog.

### 2. Configure your client

**Claude Code (CLI)**

```bash
claude mcp add perceptis --transport http https://app.perceptis.ai/mcp/v1 \
  -H "Authorization: Bearer sk-live-per-YOUR_KEY"
```

**Claude Desktop**

Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "perceptis": {
      "url": "https://app.perceptis.ai/mcp/v1",
      "headers": {
        "Authorization": "Bearer sk-live-per-YOUR_KEY"
      }
    }
  }
}
```

**Cursor**

Open Settings → MCP, or edit `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "perceptis": {
      "url": "https://app.perceptis.ai/mcp/v1",
      "headers": {
        "Authorization": "Bearer sk-live-per-YOUR_KEY"
      }
    }
  }
}
```

## 2. Generate a slide

Ask your AI client:

> "Create a slide summarizing Q3 revenue drivers"

The assistant calls `generate_slide`, polls until the slide is ready (typically 3–7 minutes), then shows you a preview with a download link.


# Quickstart: complete deck

Generate a full multi-slide deck from a natural-language prompt.

Connect your client first — see [Authentication](/perceptis-mcp-server/authentication) and the [single-slide quickstart](/perceptis-mcp-server/quickstart-slide) for setup steps.

## Generate a deck

Ask your AI client:

> "Create a 10-slide investor deck about our AI product"

The assistant calls `generate_deck`, which runs in three phases:

1. **Storyline** — generates the outline (\~2 min).
2. **Render** — renders each slide in parallel (\~30 s per slide; 5–10 min for a typical 10-slide deck).
3. **Export** — merges the slides into a single `.pptx` and shows you a download link (\~1 min).

A typical 10-slide deck completes in **5–15 minutes** end-to-end. Credits are charged after the storyline phase, based on the final slide count.

## Edit a slide in the deck

Once the deck is ready:

> "Update slide 3 to focus on the EMEA market"

The assistant edits only that slide.


# Tools reference

Full reference for all tools exposed by the Perceptis MCP server.

All tools are available once your client is connected. The AI selects the appropriate tool automatically based on your request.

Each tool description tells the AI to call `get_perceptis_guidelines` once per conversation, before its first other Perceptis tool call.

## get\_perceptis\_guidelines

Returns the Perceptis guidelines as Markdown text. They say who decides the visual design, when the AI asks you before it generates, and which design rules Perceptis keeps fixed.

* **Cost:** Free

## generate\_slide

Generates a single PowerPoint slide from a natural-language prompt, or appends a slide to an existing session.

* **Cost:** 1 credit
* **Typical duration:** usually within 4 minutes
* **Parameters:** `instruction` (string), `template_id` (optional string), `session_id` (optional string; appends the slide to that session)

## edit\_session\_slide

Edits one slide in a session: a single-slide session, a multi-slide session, or a rendered deck.

* **Cost:** your organization's free daily edits first, then 1 credit. A failed edit is refunded.
* **Typical duration:** usually within 4 minutes
* **Parameters:** `session_id`, `slide_number` (1-based), `instruction` (string)

## generate\_deck

Starts a multi-slide deck and writes its storyline. This is phase 1 of 3: storyline (`generate_deck`), render (`render_deck`), export (`export_session`). The AI chains the calls automatically.

* **Cost:** none at this step. Credits are charged when `render_deck` starts.
* **Typical duration:** usually within 18 minutes for the full deck (6 minutes for the storyline, then 12 minutes to render the slides)
* **Parameters:** `instruction` (string), `template_id` (optional string)

## render\_deck

Renders the slides of a deck whose storyline is ready (phase 2 of 3).

* **Cost:** 1 credit per slide, charged when the render starts for the first time
* **Typical duration:** usually within 12 minutes
* **Parameters:** `deck_id`

## export\_session

Merges the rendered slides of a session or deck into one `.pptx` (phase 3 of 3 for a deck). The download link is valid for 1 hour.

* **Cost:** Free
* **Parameters:** `session_id` or `deck_id`

## import\_pptx

Brings an existing `.pptx` into your workspace as an editable session. The tool returns a short-lived upload URL, and the AI uploads the file to it. Files must be `.pptx` and at most 200 MB.

* **Cost:** Free

## check\_generation\_status

Reports the status of a slide, deck or import without changing it. The AI calls it to poll a generation that returned `status: "processing"`. You do not need to invoke it yourself.

* **Cost:** Free
* **Parameters:** `session_id` or `deck_id`; add `slide_id` with `session_id` for one slide in a multi-slide session

## show\_slide\_preview

Shows a preview image of each slide and a download link for the merged `.pptx`. The AI calls it after generation completes.

* **Cost:** Free
* **Parameters:** `session_id` or `deck_id`

## get\_connected\_account

Shows which Perceptis account and organization this connection is signed in as.

* **Cost:** Free

## list\_templates

Lists PowerPoint templates available to your organization. The default template is flagged with `is_default: true`.

* **Cost:** Free
* **Parameters (all optional):**
  * `template_id` — look up a single template by id.
  * `query` — filter by template name (case-insensitive substring match).


# Credits and limits

Credit costs, polling limits, rate limits, and download link expiry.

## Credit costs

| Operation               | Cost               |
| ----------------------- | ------------------ |
| Generate a single slide | 1 credit           |
| Generate a deck         | 1 credit per slide |
| Any edit                | Free               |

Credits are shared across your organization. View your balance from the main app. Deck credits are charged after the storyline phase — the final count depends on how many slides the AI generates.

## Timeouts

Each polling window lasts \~3 minutes. If generation is still running, the AI automatically retries (up to 10 times) until the result is ready or an error occurs.

## Rate limits

The MCP surface applies a per-user sliding-window limit so a runaway client loop can't exhaust capacity for the rest of your org:

| Tool category                                                                                                                | Limit                |
| ---------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| Generation and edit tools (`generate_slide`, `edit_slide`, `generate_deck`, `render_deck`, `export_deck`, `edit_deck_slide`) | 60 calls per minute  |
| Read tools (`check_generation_status`, `show_slide_preview`, `list_templates`)                                               | 600 calls per minute |

When a limit is exceeded the tool returns `{"error": "...", "code": "rate_limited", "retry_after_seconds": 60}`. Wait a minute, then retry.

## Download links

* **Single-slide download links** expire after **30 minutes**.
* **Deck download links** (the merged `.pptx`) expire after **1 hour**.

If a link expires, ask your AI client to show the slide or deck preview again to get a fresh one.


# Error codes

Error codes returned by the Perceptis MCP server.

Tool calls that fail return an `{"error": "...", "code": "..."}` payload. The `code` is stable and safe to switch on.

## Auth and quota

| Code                   | Meaning                                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `insufficient_credits` | Organization credit balance is too low to start generation.                                                                                |
| `rate_limited`         | Per-user rate limit exceeded — see [Credits and limits](/perceptis-mcp-server/credits-and-limits). Payload includes `retry_after_seconds`. |

HTTP `401` (no body) means the bearer API key is missing, invalid, or revoked, or the OAuth session has expired.

## Lookup failures

| Code                 | Meaning                                                                                                          |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `template_not_found` | The template id does not exist or is not accessible to your organization.                                        |
| `session_not_found`  | Session id does not exist or belongs to a different user.                                                        |
| `deck_not_found`     | Deck id does not exist or belongs to a different user.                                                           |
| `slide_not_found`    | Slide number is out of range for the deck.                                                                       |
| `org_not_found`      | The authenticated user has no active organization.                                                               |
| `missing_parameter`  | A required id (`session_id` or `deck_id`) was not supplied to `check_generation_status` or `show_slide_preview`. |

## Generation state

| Code                  | Meaning                                                                                                        |
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
| `generation_failed`   | The underlying generation workflow failed. The session/deck cannot be recovered; start a new one.              |
| `session_empty`       | The session has no slides or successful versions yet.                                                          |
| `storyline_not_ready` | `render_deck` was called before the storyline phase finished. Poll `check_generation_status(deck_id=…)` first. |
| `render_not_complete` | `export_deck` was called before all slides finished rendering. Poll first.                                     |
| `deck_not_rendered`   | `edit_deck_slide` was called against a deck that has not been rendered yet.                                    |
| `artifacts_failed`    | `show_slide_preview` could not fetch the rendered PNG from storage. Retry; if persistent, regenerate.          |
| `export_timeout`      | The `.pptx` merge workflow did not finish within the polling window. Retry `export_deck`; it is idempotent.    |


# Reference overview

Endpoint reference for Perceptis API v1.

All paths use this base shape:

```
{PERCEPTIS_API_BASE_URL}/api/v1/...
```

Set `PERCEPTIS_API_BASE_URL` to the API origin only, for example `https://app.perceptis.ai`.

| Page                                                                                                                                        | Endpoint                               |
| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| [POST /api/v1/generate](/api-reference/post-generate)                                                                                       | Start a generation job                 |
| [POST /api/v1/edit](https://github.com/Whiteboard-Intelligence/perceptis/tree/main/docs/public/developer/public-api/reference/post-edit.md) | Edit a .pptx you upload                |
| [GET /api/v1/status/{job\_id}](/api-reference/get-status)                                                                                   | Poll job status and downloads          |
| [Key management](/api-reference/keys)                                                                                                       | Create, store, rotate, and revoke keys |

For examples that make full requests, see the [single-slide quickstart](/perceptis-api-v1/quickstart-single-slide), [complete-deck quickstart](/perceptis-api-v1/quickstart-deck), and [example script](/perceptis-api-v1/examples).


# POST /api/v1/generate

POST /api/v1/generate endpoint reference.

## Purpose

Start an asynchronous single-slide or complete-deck generation job. The response returns a `job_id` immediately; use [`GET /api/v1/status/{job_id}`](/api-reference/get-status) to poll until the job completes or fails.

`Idempotency-Key` is a client-supplied retry key for `POST /generate`. It is separate from the Perceptis-generated `job_id`, which is used for status polling and downloads after the job is accepted.

## Request

| Item            | Value                                                                         |
| --------------- | ----------------------------------------------------------------------------- |
| Method and path | `POST {PERCEPTIS_API_BASE_URL}/api/v1/generate`                               |
| Auth            | `Authorization: Bearer <api-key>`                                             |
| Headers         | `Content-Type: application/json`; optional `Idempotency-Key` for safe retries |
| Body            | JSON generation request                                                       |

## Request body

| Field                | Type    | Required | Notes                                                                               |
| -------------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
| `prompt`             | string  | Yes      | Instructions for the slide or deck.                                                 |
| `output_type`        | string  | Yes      | `single_slide` or `deck`.                                                           |
| `template_name`      | string  | No       | Name of an accessible Perceptis template. Omit to use automatic template selection. |
| `variant_count`      | integer | No       | Single-slide requests only.                                                         |
| `reference_images`   | array   | No       | Single-slide requests only. Each item includes base64 `data` and `mime_type`.       |
| `use_web_search`     | boolean | No       | Allow Perceptis to use web search when generating.                                  |
| `use_knowledge_base` | boolean | No       | Allow Perceptis to use your organization knowledge base when generating.            |

## Example

```json
{
  "prompt": "Create an executive summary slide for Q3 revenue growth drivers",
  "output_type": "single_slide",
  "variant_count": 1,
  "template_name": "Acme Executive",
  "use_web_search": false,
  "use_knowledge_base": true
}
```

## Responses

| HTTP  | Meaning                                                                                                                                                                |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200` | Job accepted. Body includes `job_id`, `status`, `output_type`, `downloads`, and `error`.                                                                               |
| `400` | Invalid request.                                                                                                                                                       |
| `401` | Missing, invalid, or revoked API key.                                                                                                                                  |
| `403` | Organization is not eligible for API access. Perceptis API usage requires a paid Business or Enterprise plan; Free, Starter, and trial organizations are not eligible. |
| `409` | Template ambiguity or idempotency conflict.                                                                                                                            |
| `429` | Rate limited. Use `Retry-After` and back off.                                                                                                                          |

Example accepted response:

```json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending",
  "output_type": "single_slide",
  "downloads": null,
  "error": null
}
```

## See also

* [Quickstart: single slide](/perceptis-api-v1/quickstart-single-slide)
* [Quickstart: complete deck](/perceptis-api-v1/quickstart-deck)
* [Templates](/perceptis-api-v1/templates)
* [Errors, rate limits, and billing](/perceptis-api-v1/errors-and-limits)


# GET /api/v1/status/{job\_id}

GET /api/v1/status/{job\_id} endpoint reference.

## Purpose

Return the current state of a generation job created with the same API key. When `status` is `completed`, the response includes short-lived download URLs for `.pptx` output.

## Request

| Item            | Value                                                 |
| --------------- | ----------------------------------------------------- |
| Method and path | `GET {PERCEPTIS_API_BASE_URL}/api/v1/status/{job_id}` |
| Auth            | `Authorization: Bearer <api-key>`                     |
| Path parameter  | `job_id` from the generate response                   |

## Status values

| Status       | Meaning                                                                                                                                                  |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`    | The job has been accepted.                                                                                                                               |
| `processing` | The job is being generated.                                                                                                                              |
| `completed`  | The PowerPoint output is ready to download. A deck can hold fewer slides than you asked for. Read `downloads[0].slide_count` for the number in the file. |
| `failed`     | Generation failed. See the `error` object.                                                                                                               |

## Responses

| HTTP  | Meaning                                                                                                                                                                |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200` | Status payload returned.                                                                                                                                               |
| `401` | Missing, invalid, or revoked API key.                                                                                                                                  |
| `403` | Organization is not eligible for API access. Perceptis API usage requires a paid Business or Enterprise plan; Free, Starter, and trial organizations are not eligible. |
| `404` | Unknown job, or the job was not created with this API key.                                                                                                             |
| `429` | Polling too quickly. Use `Retry-After` and back off.                                                                                                                   |

Example completed response:

```json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "output_type": "single_slide",
  "downloads": [
    {
      "url": "https://app.perceptis.ai/downloads/550e8400-e29b-41d4-a716-446655440000.pptx?expires=3600&signature=...",
      "format": "pptx",
      "variant": 1
    }
  ],
  "error": null
}
```

Download URLs are short-lived. For completed jobs, poll this endpoint again whenever you need fresh links. Downloads can be refreshed while the generated files remain available, but only with the same API key that created the job.

## See also

* [Errors, rate limits, and billing](/perceptis-api-v1/errors-and-limits)
* [Quickstart: single slide](/perceptis-api-v1/quickstart-single-slide) (polling example)


# Key management

API key management guidance for the Perceptis API.

API keys are created and managed in **Developer Settings** in the Perceptis web app. Use them to authenticate calls to `POST /api/v1/generate` and `GET /api/v1/status/{job_id}`.

Key creation and API usage require a paid Business or Enterprise organization plan. Free, Starter, and trial organizations are not eligible.

## Create a key

1. Open **Developer Settings**.
2. Create a new API key.
3. Copy the plaintext key when it is shown.
4. Store it in a secret manager or environment variable.

The plaintext key is shown only once. If you lose it, create a replacement key and revoke the old one.

## Use a key

Send the key as a bearer token:

```http
Authorization: Bearer <your-api-key>
```

A job can be polled only with the same API key that created it.

## Revoke a key

Revoke keys you no longer use from **Developer Settings**. Revoked keys stop authenticating Perceptis API requests and receive `401`.

## Recommended practices

* Use separate keys for separate integrations.
* Rotate keys on a regular schedule.
* Revoke keys immediately if they may have been exposed.
* Do not store keys in source control, client-side code, logs, or analytics tools.

## See also

* [Authentication](/perceptis-api-v1/authentication)
* [Base URLs and environment](/perceptis-api-v1/infra-routing-and-env)


