---
title: "Streamed upload with a ticket (paid UI upload)"
method: POST
path: "/api/v2/jobs/upload"
tags: ["v2 Jobs"]
---

# Streamed upload with a ticket (paid UI upload)

`POST /api/v2/jobs/upload`

## Request

- Base URL: `https://pdf2md.dev`
- URL: `https://pdf2md.dev/api/v2/jobs/upload`
- Auth: API key in header `Upload-Ticket`

EP-048. Authenticated ONLY by a one-time Upload-Ticket header obtained from POST /api/v2/jobs/upload-ticket (no per-body ES256 signature, which is why this lane can STREAM, unlike v1 /jobs). Enforced at the ticket's per-tier byte cap; creates an owner_type=account_device job for the ticket's account. Single-use: the ticket is consumed on arrival.

## Response `200`

Idempotent repeat — existing job (same Idempotency-Key)

- V2Job
  - `job_id` string, uuid, required
  - `status` 'queued' | 'processing' | 'ready' | 'error' | 'canceled' | 'deleted', required: Job status
  - `file_name` string, required
  - `created_at` string, date-time, required
  - `status_since` string, date-time
  - `pages` integer, nullable
  - `truncated` boolean
  - `output_size` integer, nullable
  - `download_url` string, uri, nullable: populated for ready
  - `tier` 'anonymous' | 'free' | 'lifetime' | 'builder' | 'pro' | 'business' | 'enterprise', required: EP-069: `lifetime` is the one-time unlock, ranked BETWEEN free and builder — it raises the interactive limits and carries 600 pages/month, but grants no API keys, hosted MCP, webhooks or batch, so a subscription is genuinely more and its holder may still buy one.
  - `slot_usage` object
    - `used` integer
    - `limit` integer
  - `external_id` string, nullable
  - `tags` object, nullable

## Other responses

- `201`: Created
- `400`: Invalid data (not a PDF, no url/file, broken link)
- `401`: Missing, invalid, expired, or already-used upload ticket
- `409`: slots_full — all slots are in use (clears as jobs finish) | quota_exceeded — the monthly page allowance is spent (clears on the 1st). EP-070-T7: the quota body also carries `used`, `limit` and `reset_at` (RFC3339, start of the next UTC month) so a client can say which limit and when, in its own language. 409 rather than 429 on purpose: 429 renders as "retry in a moment", which is wrong for something that returns next month.
- `413`: File exceeds the tier limit (max_file_size)

---

[API](https://skmtc.dev/pdf2md/apis/pdf-to-markdown-api-facade.md) · [All operations](https://skmtc.dev/pdf2md/apis/pdf-to-markdown-api-facade/llms.txt) · [OpenAPI document](https://skmtc.dev/pdf2md/apis/pdf-to-markdown-api-facade/revisions/4636782fb8b6?raw)
