> For the complete documentation index, see [llms.txt](https://docs.twistly.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.twistly.ai/api-reference/presentations.md).

# Presentation API

Generate professional presentations from a topic, raw text, or a source document (PDF / PPTX / DOCX / TXT) — programmatically, outside PowerPoint. Output is a downloadable PPTX or PDF.

|                  |                                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| **Base URL**     | `https://api.appsdowonders.com`                                                                |
| **Availability** | Pro and Team plans — see [API Access](/plans-billing-and-team/api-access.md) for getting a key |
| **Style**        | Asynchronous: submit a job, poll for the result                                                |

The API inherits the add-in's generation pipeline — the same [templates](/create-a-presentation/templates.md), [image sources and styles](/create-a-presentation/images.md), and quality.

{% hint style="info" %}
Building an AI-assistant integration instead of a code integration? See the [Twistly MCP Server](/api-reference/twistly-mcp-server.md) — same pipeline, no API key.
{% endhint %}

## Authentication

Every request must carry your API key (starts with `ppsk_`), one of two ways:

| Method                        | Header                         | Notes                                        |
| ----------------------------- | ------------------------------ | -------------------------------------------- |
| Bearer auth (**recommended**) | `Authorization: Bearer ppsk_…` |                                              |
| API-key header                | `x-api-key: ppsk_…`            | For platforms that can't set a Bearer header |

Keys are issued in the add-in (**Settings → API key**, Pro/Team) — see [API Access](/plans-billing-and-team/api-access.md). **One active key per account**: to rotate, revoke the old key first, then create a new one.

## Asynchronous flow

Every `POST` endpoint queues a generation job and returns `202 Accepted` with a job descriptor. Poll `GET /v1/presentations/{id}` (recommended cadence: **every 2–3 seconds**) until `status` becomes `completed` — download URL in `result.url` — or `failed`, with the reason in `error`.

* Typical generation time is **30–90 seconds**; the hard cap is **5 minutes** per job, after which jobs are marked `failed` with code `TIMEOUT`.
* Download links are **permanent**.
* Jobs are **owner-scoped** — visible only to the API key that created them.
* Job `status` is one of `queued`, `processing`, `completed`, `failed`.

```bash
# 1. Submit a job
curl -s -X POST https://api.appsdowonders.com/v1/presentations/topic \
  -H "Authorization: Bearer ppsk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "topic": "Renewable Energy", "numSlides": 10 }'
# → 202 { "id": "9f1c…", "status": "queued", "createdAt": "…" }

# 2. Poll until completed (every 2–3 s)
curl -s https://api.appsdowonders.com/v1/presentations/9f1c2a4e-6e2c-4f8a-9b3d-1a2b3c4d5e6f \
  -H "Authorization: Bearer ppsk_YOUR_KEY"
# → 200 { "status": "completed", "result": { "url": "https://…/renewable-energy-9f1c2a4e.pptx" } }
```

## Rate limits

Two separate buckets per API key:

| Bucket         | Limit                 | Applies to                               |
| -------------- | --------------------- | ---------------------------------------- |
| `public-write` | **3 requests/min**    | All `POST /v1/presentations/*` endpoints |
| `public-read`  | **1200 requests/min** | `GET /v1/presentations/{id}`             |

Every response includes `x-ratelimit-limit`, `x-ratelimit-remaining`, and `x-ratelimit-reset` (milliseconds until the window resets). Exceeding a bucket returns `429 RATE_LIMIT_EXCEEDED`. The read limit comfortably supports the 2–3 s polling cadence, even across many concurrent jobs.

## Shared parameters

These fields are accepted by all creation endpoints (see each endpoint for which apply):

| Field         | Values                                                                                                        | Default    | Notes                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------- | ------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `numSlides`   | `-1` (auto) or `1`–`50`                                                                                       | `-1`       | `0` is invalid. Auto lets the AI fit the count to the content — see [Number of Slides](/create-a-presentation/number-of-slides.md)                                                                                                                                                                                                                                                               |
| `template`    | Template id                                                                                                   | `Tokyo`    | Case-sensitive, no spaces: the template name with spaces removed (**New York** → `NewYork`). All **51 built-in templates** are available — see [Templates](/create-a-presentation/templates.md) for the gallery and the [full id list](/api-reference/twistly-mcp-server.md#templates). Custom uploaded templates are **not** available — jobs referencing them fail with `UNSUPPORTED_TEMPLATE` |
| `format`      | `pptx` \| `pdf`                                                                                               | `pptx`     | Output file format                                                                                                                                                                                                                                                                                                                                                                               |
| `imageSource` | `unsplash` \| `pexels` \| `scrapingdog` \| `ai` \| `flux`                                                     | `unsplash` | `unsplash`/`pexels` are stock, `scrapingdog` is web search, `ai`/`flux` are AI-generated — see [Images in Your Presentation](/create-a-presentation/images.md)                                                                                                                                                                                                                                   |
| `imageStyle`  | `auto` \| `photorealistic` \| `illustration` \| `neon` \| `abstract` \| `black-and-white` \| `3d` \| `linear` | —          | Only when `imageSource` is `ai` or `flux` — the same styles as the add-in's [AI image styles](/create-a-presentation/images.md#ai-image-styles)                                                                                                                                                                                                                                                  |

## POST /v1/presentations/topic

Create a presentation from a topic — you have a topic or idea, the AI writes the content.

**Body** (`application/json`) — shared parameters above, plus:

| Field              | Type                                                                             | Required | Default     | Notes                                    |
| ------------------ | -------------------------------------------------------------------------------- | -------- | ----------- | ---------------------------------------- |
| `topic`            | string                                                                           | **Yes**  | —           | Subject of the presentation, ≤ 450 chars |
| `language`         | string                                                                           | No       | auto-detect | Output language                          |
| `content`          | `short` \| `detailed` \| `bulletPoints`                                          | No       | `detailed`  | Text verbosity                           |
| `presentationType` | `General` \| `Educational Project` \| `Business Pitch` \| `Reports and Insights` | No       | —           |                                          |
| `targetAudience`   | string                                                                           | No       | —           | Free text, ≤ 200 chars                   |
| `toneAndStyle`     | string                                                                           | No       | —           | Free text, ≤ 200 chars                   |

**Examples:**

```json
{ "topic": "Renewable Energy" }
```

```json
{
  "topic": "История кофе",
  "language": "russian",
  "numSlides": 12,
  "template": "Berlin"
}
```

```json
{
  "topic": "Digital Marketing Strategy",
  "numSlides": 5,
  "template": "Berlin",
  "imageSource": "flux",
  "imageStyle": "illustration",
  "content": "bulletPoints",
  "presentationType": "Business Pitch",
  "targetAudience": "Marketing managers",
  "toneAndStyle": "Enthusiastic"
}
```

## POST /v1/presentations/text

Create a presentation from raw text. **The output language always matches the source language.**

**Body** (`application/json`) — shared parameters above, plus:

| Field                | Type                                    | Required | Default   | Notes                                                                                    |
| -------------------- | --------------------------------------- | -------- | --------- | ---------------------------------------------------------------------------------------- |
| `text`               | string                                  | **Yes**  | —         | Source text, max **665,000 tokens** (≈ 500,000 words)                                    |
| `contentModificator` | `preserve` \| `expand` \| `condense`    | No       | `expand`  | How the source content is treated: keep as-is, add detail, or tighten into key takeaways |
| `textDensity`        | `minimal` \| `concise` \| `detailed`    | No       | `concise` | Density of text on each slide                                                            |
| `verbosity`          | `short` \| `detailed` \| `bulletPoints` | No       | —         | Text verbosity                                                                           |

If the text exceeds the token limit, the API responds `400 BAD_REQUEST` with `tokensCount` and `tokenLimit` in the error body.

**Example** — text to a 10-slide PDF:

```json
{
  "text": "Coffee was first discovered in the Ethiopian highlands...",
  "numSlides": 10,
  "template": "Rome",
  "format": "pdf"
}
```

## POST /v1/presentations/file

Create a presentation from a document. Upload a PDF, PPTX, DOCX, or TXT (**max 300 MB**). File types are validated by magic-byte sniffing — extension spoofing is rejected. **The output language always matches the source language.**

**Body** (`multipart/form-data`) — every non-file field arrives as a string (`numSlides` as `"15"`); the API coerces them. Shared parameters above, plus:

| Field                | Type   | Required | Notes                                                                  |
| -------------------- | ------ | -------- | ---------------------------------------------------------------------- |
| `file`               | binary | **Yes**  | PDF (`application/pdf`), PPTX, DOCX, or TXT (`text/plain`), max 300 MB |
| `contentModificator` | string | No       | `preserve` \| `expand` \| `condense` (default `expand`)                |
| `textDensity`        | string | No       | `minimal` \| `concise` \| `detailed` (default `concise`)               |
| `verbosity`          | string | No       | `short` \| `detailed` \| `bulletPoints`                                |

**Example:**

```bash
curl -s -X POST https://api.appsdowonders.com/v1/presentations/file \
  -H "Authorization: Bearer ppsk_YOUR_KEY" \
  -F "file=@quarterly-report.pdf" \
  -F "numSlides=15" \
  -F "template=Berlin"
```

## GET /v1/presentations/{id}

Get job status and result. Once `status` is `completed`, fetch the permanent download URL from `result.url`.

| Parameter | In   | Type | Notes                                                    |
| --------- | ---- | ---- | -------------------------------------------------------- |
| `id`      | path | UUID | Job id returned by a `POST /v1/presentations/*` endpoint |

**Job object:**

| Field                         | Type                                                | Present                                                    |
| ----------------------------- | --------------------------------------------------- | ---------------------------------------------------------- |
| `id`                          | UUID                                                | always                                                     |
| `status`                      | `queued` \| `processing` \| `completed` \| `failed` | always                                                     |
| `createdAt`                   | ISO-8601                                            | always                                                     |
| `completedAt`                 | ISO-8601                                            | when `status` is `completed` or `failed`                   |
| `result.url`                  | URL                                                 | when `completed` — permanent download URL for the PPTX/PDF |
| `error.code`, `error.message` | see below                                           | when `failed`                                              |

**Response examples:**

```json
{
  "id": "9f1c2a4e-6e2c-4f8a-9b3d-1a2b3c4d5e6f",
  "status": "completed",
  "createdAt": "2026-06-03T10:15:00.000Z",
  "completedAt": "2026-06-03T10:15:42.000Z",
  "result": {
    "url": "https://s3.amazonaws.com/.../the-history-of-coffee-9f1c2a4e.pptx"
  }
}
```

```json
{
  "id": "9f1c2a4e-6e2c-4f8a-9b3d-1a2b3c4d5e6f",
  "status": "failed",
  "createdAt": "2026-06-03T10:15:00.000Z",
  "completedAt": "2026-06-03T10:20:00.000Z",
  "error": {
    "code": "UPSTREAM_FAILURE",
    "message": "human-readable reason"
  }
}
```

Returns `404 NOT_FOUND` for unknown ids (or jobs belonging to another key).

**Job failure codes** (`error.code` on a failed job):

| Code                   | Meaning                                                               |
| ---------------------- | --------------------------------------------------------------------- |
| `TIMEOUT`              | Generation exceeded the soft timeout                                  |
| `WORKER_TIMEOUT`       | Worker aborted the job (default 5 min)                                |
| `UPSTREAM_FAILURE`     | Underlying AI/build pipeline failed — retry                           |
| `UNSUPPORTED_TEMPLATE` | Custom (user-uploaded) templates are not supported via the public API |
| `INPUT_TOO_LARGE`      | Source content exceeded internal processing limits                    |

## GET /v1/me

Lightweight key-validation endpoint. Returns the account associated with the API key — useful as an authentication test (e.g., for Zapier or other integration platforms) and for checking remaining credits.

```bash
curl -s https://api.appsdowonders.com/v1/me \
  -H "Authorization: Bearer ppsk_YOUR_KEY"
```

**Response** `200`:

```json
{
  "userId": 1,
  "email": "user@example.com",
  "plan": "pro",
  "credits": {
    "remaining": 100
  }
}
```

`credits.remaining` is `null` for internal/unlimited keys. Returns `401 UNAUTHORIZED` if the key is missing or invalid.

## HTTP errors

All `4xx`/`5xx` responses use a uniform envelope:

```json
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Human-readable message. For invalid enum fields, lists the allowed values.",
    "status": 400
  }
}
```

| HTTP status | `error.code`                                                                                    |
| ----------- | ----------------------------------------------------------------------------------------------- |
| 400         | `BAD_REQUEST` — validation failed (for oversized text, includes `tokensCount` and `tokenLimit`) |
| 401         | `UNAUTHORIZED` — missing or invalid API key                                                     |
| 403         | `FORBIDDEN` — the account's plan doesn't include API access                                     |
| 404         | `NOT_FOUND` — unknown job id (or a job owned by another key)                                    |
| 422         | `UNPROCESSABLE_ENTITY` — input can't be processed                                               |
| 429         | `RATE_LIMIT_EXCEEDED` — see [Rate limits](#rate-limits)                                         |
| 500         | `INTERNAL_SERVER_ERROR`                                                                         |
