API v1StableUpdated May 20, 2026

Deckup API documentation

Deckup converts PowerPoint files into clean, structured SVG with a JSON sidecar describing what rendered and what didn't. One endpoint, two modes, predictable shape. If you've used Stripe or Resend, the rhythm here will feel familiar.

v1 is intentionally narrow: PPTX → SVG only. Reverse conversion and additional output formats (HTML, JSON-AST) ship later in 2026 — see the roadmap. Skipped element kinds are listed in the sidecar so you know exactly what to fall back on.

Quickstart

Five minutes from signup to first converted slide. Three things to know up front:

  1. API root: https://api.deckup.io
  2. Auth: Authorization: Bearer sk_live_… (or sk_test_… for test mode)
  3. One endpoint to learn: POST /v1/convert

1. Get a key

Sign up, head to Settings → API keys, copy your test key. It'll look like sk_test_01HXR… — the amber prefix everywhere in the dashboard is intentional. Test keys never count against quota and never produce billable conversions.

2. Send your first deck

curl POST "https://api.deckup.io/v1/convert?mode=sync" \
  -H "Authorization: Bearer $DECKUP_KEY" \
  -F "file=@quarterly-review.pptx"
from deckup import Deckup

client = Deckup(api_key=os.environ["DECKUP_KEY"])

with open("quarterly-review.pptx", "rb") as f:
    conv = client.conversions.create(file=f, mode="sync")

print(conv.id, conv.status, len(conv.slides))
import { Deckup } from "deckup";
import fs from "node:fs";

const deckup = new Deckup({ apiKey: process.env.DECKUP_KEY });

const conv = await deckup.conversions.create({
  file: fs.createReadStream("quarterly-review.pptx"),
  mode: "sync",
});

console.log(conv.id, conv.status, conv.slides.length);
package main

import (
    "context"
    "os"
    deckup "github.com/deckup/deckup-go"
)

func main() {
    client := deckup.NewClient(os.Getenv("DECKUP_KEY"))
    f, _ := os.Open("quarterly-review.pptx")
    defer f.Close()

    conv, _ := client.Conversions.Create(context.TODO(), &deckup.CreateParams{
        File: f,
        Mode: deckup.ModeSync,
    })
    fmt.Println(conv.ID, conv.Status, len(conv.Slides))
}

3. Handle the response

Sync responses come back in roughly 5–10 seconds for typical decks. The body is the full sidecar JSON with signed SVG URLs that expire in 5 minutes — fetch them, store them, mirror them to your own bucket if you need to keep them.

{ "id": "conv_01HXR3K4P7T1G2Y9", "status": "succeeded", "mode": "sync", "slides": [ { "index": 0, "title": "Q4 Business Review", "svg_url": "https://cdn.deckup.io/conv_01HXR3…/slide_01.svg", "dimensions": { "w": 1280, "h": 720 } } ], "skipped": [], "created": 1747736102, "livemode": false }

Authentication

All requests are authenticated with a bearer token in the Authorization header.

request headers
Authorization: Bearer sk_live_01HXR3…
Content-Type:  multipart/form-data
Idempotency-Key: a8f3c1e0-…  // optional, recommended

Keys are scoped to a single account. Multiple keys per account are supported — rotate freely; old keys keep working during a 24-hour overlap window when you regenerate.

Never embed a sk_live_… key in client-side JavaScript or commit it to a repo. Use server-side credentials only. We surface any key that appears in a public commit and rotate it automatically — but don't make us.

Test mode

Test-mode keys (sk_test_…) are free forever and produce identical output to live mode, except that:

The whole point: integrate end-to-end without paying for it, then flip a single environment variable when you go live.

Create a conversion

POST /v1/convert

The one endpoint that does the work. Accept a multipart upload, pick a mode, optionally provide a webhook URL.

Body parameters

NameTypeDescription
file *fileThe PPTX file to convert. Up to 5 MB on Free, 250 MB on Scale. Anything bigger returns 413 Payload Too Large.
mode *stringEither sync (block for the response, typical 5–10s) or async (return immediately, callback by webhook).
webhook_urlstringRequired when mode=async. Must be HTTPS. We POST a signed payload here when the conversion completes.
webhook_secretstringPer-call signing secret. Overrides the account-level secret. Recommended for multi-tenant routing.
metadataobjectUp to 20 key-value pairs (string keys, string values, <500 chars total). Returned verbatim on the conversion object — use for your own row IDs or correlation tokens.

Returns

A Conversion object. Status will be succeeded or partial for sync mode; queued for async mode (the eventual result comes via webhook).

Example

curl POST "https://api.deckup.io/v1/convert?mode=async" \
  -H "Authorization: Bearer $DECKUP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@huge-training-deck.pptx" \
  -F "webhook_url=https://acme.com/hooks/deckup" \
  -F 'metadata[customer_id]=cus_2L8K9'
conv = client.conversions.create(
    file=open("huge-training-deck.pptx", "rb"),
    mode="async",
    webhook_url="https://acme.com/hooks/deckup",
    metadata={"customer_id": "cus_2L8K9"},
)
assert conv.status == "queued"
const conv = await deckup.conversions.create({
  file: fs.createReadStream("huge-training-deck.pptx"),
  mode: "async",
  webhookUrl: "https://acme.com/hooks/deckup",
  metadata: { customer_id: "cus_2L8K9" },
});

Retrieve a conversion

GET /v1/conversions/{id}

Idempotent lookup for a conversion you've created. Useful while a webhook is in flight, or for any out-of-band reconciliation.

List conversions

GET /v1/conversions

Paginated list of conversions on your account. Cursor-based pagination via starting_after and ending_before query params.

Query parameters

NameTypeDescription
limitint1–100, default 25.
statusstringFilter by status: queued, running, succeeded, partial, failed.
livemodebooleanFilter to live (true) or test (false) conversions.
created.gteintUnix timestamp. Returns only conversions created on or after this time.

The sidecar JSON

Every conversion returns a sidecar describing what rendered, what didn't, and the metadata you'll need to display alongside. Schema is versioned via sidecar_version — pin against it if you're paranoid about breakage.

FieldTypeDescription
idstringConversion identifier. Prefixed conv_ (or conv_test_).
statusstringsucceeded, partial, failed, queued, running.
slides[]arrayOne object per slide. Has index, title, notes, dimensions, svg_url, and optionally skipped[].
skipped[]arrayAggregate of all skipped elements across the deck. Each entry has slide, kind, and reason.
metadataobjectWhatever metadata you passed at create time, plus our processing metadata under _deckup.
livemodebooleantrue for live keys, false for test keys.

Webhook events

Async conversions complete via a signed webhook POST to the URL you provided. The body is the full conversion object.

EventWhen it fires
conversion.succeededAll slides rendered without any unsupported elements.
conversion.partialAt least one slide rendered, but some elements were skipped. skipped[] tells you what.
conversion.failedNo slides could be rendered (corrupt file, unsupported PPTX version, etc.).

Verifying webhook signatures

Every webhook delivery carries a Deckup-Signature header with a timestamp and an HMAC-SHA256 signature of the payload. Verify it before trusting the body.

import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const [t, sig] = header.split(",").map(p => p.split("=")[1]);
  const signed = `${t}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signed)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(sig,      "hex"),
  );
}
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=") for p in header.split(","))
    signed = f"{parts['t']}.{raw_body.decode()}"
    expected = hmac.new(
        secret.encode(), signed.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func Verify(body []byte, header, secret string) bool {
    // Parse "t=…,v1=…" and HMAC-SHA256 over "t.body".
    // Use hmac.Equal — never bytes.Equal — for timing safety.
}
Reject any payload where the timestamp is more than 5 minutes old. This is a defense against replayed deliveries that pre-date a key rotation.

Retry schedule

If your endpoint returns a non-2xx response or times out, Deckup retries on this schedule:

Dead-lettered deliveries are surfaced in the dashboard with one-click replay. Once your endpoint is back, fire the whole backlog from a single button.

Errors

Conventional HTTP status codes with a JSON body describing the failure. Error codes are stable strings you can match against — names will not change between v1 minor versions.

StatusError codeMeaning
400invalid_requestMalformed body, missing required parameter, or unparseable PPTX file.
401authentication_failedMissing, malformed, or revoked API key.
402quota_exhaustedMonthly quota hit and overages are not enabled. Upgrade or enable overages.
413file_too_largeFile exceeds your tier's max size. Upgrade or split the deck.
429rate_limitedPer-key request rate limit. Retry with exponential backoff.
5xxinternal_errorOur fault. Retry. If it persists, the status page will say so.

Rate limits

Per API key, regardless of tier: 60 requests per minute, 1,000 requests per hour. Burstable — short spikes are absorbed by the queue. Long-running async jobs do not count against these limits once accepted.

Need more? Email support@deckup.io with your use case. Per-key overrides are part of the dashboard for Pro and Scale customers.

SDKs & libraries

Official SDKs are auto-generated from our OpenAPI spec. The HTTP shape is the source of truth — every SDK exposes the same surface area, just shaped to the host language.

LanguagePackageStatus
Pythonpip install deckupbeta
Node / TypeScriptnpm install deckupbeta
Gogo get github.com/deckup/deckup-gopreview
Rubygem install deckupplanned

Changelog

Stable changes are dated, versioned, and announced via the dashboard and the public changelog page. We use semver for the SDKs and a date-stamped API version (2026-05-13-style) for the API itself; pin to a date if you want to lock against shape changes.


Next steps
Open the playground →
Drop a deck, see the response, copy the curl.
Something off?
Email the developer →
Hours, not days. We read everything in v1.