---
title: "Apply a recipe to a document"
method: POST
path: "/pdf/edit"
tags: ["PDF editor"]
---

# Apply a recipe to a document

`POST /pdf/edit`

## Request

- Base URL: `https://api.podpdf.com`
- URL: `https://api.podpdf.com/pdf/edit`
- Auth: API key in header `X-API-Key`

Applies the operations in order and stores the new document, returning a download link
valid for one hour (the file is kept for 7 days). All or nothing: if any operation fails,
nothing is written and nothing is charged. One charge per save, however many operations
it carries. Saving rewrites the whole file, so an existing digital signature stops
validating — preflight reports that as `signatures_invalidated`.

Send a `commit_token` to make retries safe: repeating a save with the same token returns
the first result (`repeated: true`) instead of charging again.

## Request body

- EditRequest: The document — `source_key` from `/pdf/upload-url` (up to 150 MB) or `input` inline (up to 4 MB) — and the recipe to apply to it.
  - `source_key` string: Key from `/pdf/upload-url`. Give this or `input`.
  - `filename` string: Name of the document, with `source_key`.
  - `input` InlinePdf: A PDF sent with the request instead of uploaded first. Up to 4 MB.
    - `pdf_base64` string, byte, required
    - `filename` string
  - `recipe` EditOperation[], required: Operations, applied in order. At most 200.
    - union: One step of a recipe. `op` says which.
      - PagesRotateOp: Rotate pages. Absolute by default; `relative` adds to the current rotation.
        - `op` 'pages.rotate', required
        - `pages` union: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `degrees` integer, required
        - `relative` boolean
      - PagesDeleteOp: Delete pages. `pages` is required, and deleting every page is refused. Form fields left with no page are removed too.
        - `op` 'pages.delete', required
        - `pages` union, required: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
      - PagesReorderOp: Put the pages in a new order. `order` must list every page exactly once.
        - `op` 'pages.reorder', required
        - `order` union, required: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
      - PagesExtractOp: Keep only these pages, in the order given, and drop the rest.
        - `op` 'pages.extract', required
        - `pages` union, required: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
      - PagesInsertOp: Insert blank pages. They get ids `n0`, `n1`… that later operations can write on.
        - `op` 'pages.insert', required
        - `at` union: A page id such as `p3`, a 1-based page number, or `end` (the default).
          - string
          - integer
        - `position` 'before' | 'after'
        - `count` integer
        - `size` union: A paper size, or `{ width, height }` in points. Defaults to the size of the neighbouring page.
          - 'A3' | 'A4' | 'A5' | 'A6' | 'Letter' | 'Legal' | 'Tabloid'
          - object
            - `width` number, required
            - `height` number, required
      - TextAddOp: Put text on pages — a name, a date, a reference. Multi-line blocks work.
        - `op` 'text.add', required
        - `pages` union: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `text` string, required
        - `size` number
        - `font` 'helvetica' | 'helvetica-bold' | 'helvetica-oblique' | 'times' | 'times-bold' | 'times-italic' | 'courier' | 'courier-bold' | 'unicode' | 'unicode-bold': The 8 standard PDF fonts, or `unicode` / `unicode-bold` (Noto Sans) for accented, Greek, Cyrillic and Vietnamese text — swapped in automatically when needed and reported as `font_substituted`. CJK and right-to-left scripts are refused.
        - `color` string
        - `opacity` number
        - `rotation` number
        - `position` 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
        - `x` number: With `y`, places the text at exact coordinates instead of `position`.
        - `y` number
        - `margin_pt` number
        - `line_height` number
      - WatermarkTextOp: A text watermark, centred by default. Same options as `text.add`.
        - `op` 'watermark.text', required
        - `pages` union: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `text` string, required
        - `size` number
        - `font` 'helvetica' | 'helvetica-bold' | 'helvetica-oblique' | 'times' | 'times-bold' | 'times-italic' | 'courier' | 'courier-bold' | 'unicode' | 'unicode-bold': The 8 standard PDF fonts, or `unicode` / `unicode-bold` (Noto Sans) for accented, Greek, Cyrillic and Vietnamese text — swapped in automatically when needed and reported as `font_substituted`. CJK and right-to-left scripts are refused.
        - `color` string
        - `opacity` number
        - `rotation` number
        - `position` 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
        - `x` number
        - `y` number
        - `margin_pt` number
        - `line_height` number
      - WatermarkImageOp: Draw an image into the page — a logo, a stamp, a signature image. Permanent; use `annotate.stamp` for one that can be removed. Size it with `width`/`height` or `scale`, not both.
        - `op` 'watermark.image', required
        - `pages` union: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `data` string, required: A PNG or JPEG, base64-encoded (a `data:image/...;base64,` prefix is accepted). Up to 4 MB.
        - `width` number
        - `height` number
        - `scale` number
        - `opacity` number
        - `rotation` number
        - `position` 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
        - `x` number
        - `y` number
        - `margin_pt` number
      - HeaderFooterOp: A header or footer. `{page}`, `{pages}` and `{date}` in the text are filled in per page, which makes it page numbering too.
        - `op` 'header_footer', required
        - `pages` union: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `text` string, required
        - `size` number
        - `font` 'helvetica' | 'helvetica-bold' | 'helvetica-oblique' | 'times' | 'times-bold' | 'times-italic' | 'courier' | 'courier-bold' | 'unicode' | 'unicode-bold': The 8 standard PDF fonts, or `unicode` / `unicode-bold` (Noto Sans) for accented, Greek, Cyrillic and Vietnamese text — swapped in automatically when needed and reported as `font_substituted`. CJK and right-to-left scripts are refused.
        - `color` string
        - `opacity` number
        - `position` 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
        - `margin_pt` number
      - BatesStampOp: Sequential Bates numbers, drawn the right way up even on pages scanned sideways. The result reports `next_start` for continuing in another document.
        - `op` 'bates.stamp', required
        - `pages` union: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `prefix` string
        - `suffix` string
        - `start` integer
        - `digits` integer
        - `size` number
        - `font` 'helvetica' | 'helvetica-bold' | 'helvetica-oblique' | 'times' | 'times-bold' | 'times-italic' | 'courier' | 'courier-bold' | 'unicode' | 'unicode-bold': The 8 standard PDF fonts, or `unicode` / `unicode-bold` (Noto Sans) for accented, Greek, Cyrillic and Vietnamese text — swapped in automatically when needed and reported as `font_substituted`. CJK and right-to-left scripts are refused.
        - `color` string
        - `opacity` number
        - `position` 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
        - `margin_pt` number
      - MetadataSetOp: Set document properties. At least one field.
        - `op` 'metadata.set', required
        - `title` string
        - `author` string
        - `subject` string
        - `keywords` string
        - `creator` string
        - `producer` string
        - `creation_date` string, date-time
        - `modification_date` string, date-time
      - MetadataStripOp: Clear the document properties and the hidden XMP metadata, and remove the objects from the file.
        - `op` 'metadata.strip', required
        - `keep` string[]: Properties to leave in place.
      - SanitizeOp: Remove active content, and report what was removed. Everything except external links by default.
        - `op` 'sanitize', required
        - `remove` string[]
      - FormFillOp: Fill form fields by their full name (from `/pdf/inspect` with `for: edit`). Text fields take a string, check boxes `true`/`false`, radio groups and dropdowns one of their options, list boxes one option or a list. XFA forms are refused.
        - `op` 'form.fill', required
        - `fields` object, required
        - `font` 'helvetica' | 'helvetica-bold' | 'helvetica-oblique' | 'times' | 'times-bold' | 'times-italic' | 'courier' | 'courier-bold' | 'unicode' | 'unicode-bold': The 8 standard PDF fonts, or `unicode` / `unicode-bold` (Noto Sans) for accented, Greek, Cyrillic and Vietnamese text — swapped in automatically when needed and reported as `font_substituted`. CJK and right-to-left scripts are refused.
      - FormFlattenOp: Make fields part of the page, so they can no longer be edited. Every field that can be, or just those named. Cannot be undone.
        - `op` 'form.flatten', required
        - `fields` string[]
        - `font` 'helvetica' | 'helvetica-bold' | 'helvetica-oblique' | 'times' | 'times-bold' | 'times-italic' | 'courier' | 'courier-bold' | 'unicode' | 'unicode-bold': The 8 standard PDF fonts, or `unicode` / `unicode-bold` (Noto Sans) for accented, Greek, Cyrillic and Vietnamese text — swapped in automatically when needed and reported as `font_substituted`. CJK and right-to-left scripts are refused.
      - AnnotateHighlightOp: Highlight either `rects` on one page (up to 200 rectangles, one annotation), or every match of `text` (one annotation per match, up to 500; all pages unless `pages` narrows it). Give one or the other. `TEXT_NOT_FOUND` if the text is not there.
        - `op` 'annotate.highlight', required
        - `pages` union: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `rects` PdfRect[]
          - `x` number, required
          - `y` number, required
          - `width` number, required
          - `height` number, required
        - `text` string
        - `match_case` boolean: Only with `text`.
        - `whole_word` boolean: Only with `text`.
        - `color` string
        - `opacity` number
        - `contents` string: A comment shown with the highlight.
        - `author` string
      - AnnotateNoteOp: A sticky note at a point on exactly one page.
        - `op` 'annotate.note', required
        - `pages` union, required: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `x` number, required
        - `y` number, required
        - `contents` string, required
        - `icon` 'Note' | 'Comment' | 'Key' | 'Help' | 'NewParagraph' | 'Paragraph' | 'Insert'
        - `color` string
        - `author` string
        - `open` boolean
      - AnnotateStampOp: An image stamp — a signature, an approval mark — as an annotation that can be removed later. On exactly one page. Size it with `width`/`height` or `scale`, not both.
        - `op` 'annotate.stamp', required
        - `pages` union, required: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `data` string, required: A PNG or JPEG, base64-encoded (a `data:image/...;base64,` prefix is accepted). Up to 4 MB.
        - `x` number, required
        - `y` number, required
        - `width` number
        - `height` number
        - `scale` number
        - `opacity` number
        - `contents` string
        - `author` string
      - AnnotationsFlattenOp: Burn annotations into the page so they can no longer be moved or removed. Form widgets, links and popups are never flattened; sticky notes are skipped and reported. `NO_ANNOTATIONS` if there is nothing to flatten.
        - `op` 'annotations.flatten', required
        - `pages` union: Which pages an operation applies to. Either a range string such as `"2,5-7"`, read against the document as it stands at that step of the recipe, or a list of page ids from `/pdf/inspect` (`p0` is the first original page; `n0`, `n1`… are pages a `pages.insert` created), which keep pointing at the same page whatever earlier operations did. Leave it out to select every page.
          - string
          - string[]
        - `types` string[]: Annotation subtypes to flatten, e.g. `Highlight`, `Stamp`. All of them if left out.
  - `commit_token` string: Makes a save safe to retry — the same token returns the first result instead of charging again. Ignored by preflight.
  - `options` object
    - `output_filename` string: Name for the saved document, without `.pdf`.

## Response `200`

The edited document

- EditResult
  - `job_id` string
  - `operation` 'pdf_edit'
  - `status` 'completed'
  - `repeated` boolean: This `commit_token` was already saved; this is the first result again, not a new charge.
  - `input_pages` integer
  - `output_pages` integer
  - `output_bytes` integer
  - `operations` EditOperationResult[]
    - `index` integer: Position in the recipe, from 0.
    - `op` string
    - `scope` 'page' | 'document', nullable
    - `resolved_pages` integer[]: 1-based page numbers the operation applied to, at that step.
    - `effects` object: Operation-specific detail — for example `next_start` for Bates, `placements` for marks, `matches` for a text highlight, `skipped` for a flatten.
    - `ok` boolean, nullable: Preflight only. `null` when the operation was not evaluated.
    - `blocking` boolean: Preflight only. This operation fails and blocks the save.
    - `not_evaluated` boolean: Preflight only. An earlier operation failed, so this one could not be checked.
    - `reasons` object[]: Preflight only. Why a blocking operation fails.
      - `code` string
      - `message` string
  - `download_url` string, uri: Valid for 1 hour. The file is kept for 7 days — fetch a new link with `GET /jobs/{job_id}/download`.
  - `download_url_expires_at` string, date-time
  - `warnings` string[]
  - `sanitize` SanitizeReport: What `sanitize` found and removed, by category.

## Other responses

- `400`: The request was invalid
- `401`: Missing or invalid API key
- `402`: `UPGRADE_REQUIRED` — the account has no plan yet
- `403`: `INSUFFICIENT_CREDITS`, `CONVERSION_TYPE_NOT_ENABLED`, `PDF_TOOLS_NOT_ENABLED`, `RATE_LIMIT_EXCEEDED` or `ACCOUNT_NOT_FOUND`. Note that a plan's rate limit is a 403, not a 429.
- `404`: No such resource for this account
- `408`: `PDFOP_TIMEOUT` — the save took longer than 25 seconds. Nothing is written or charged.
- `409`: `COMMIT_IN_PROGRESS` — a save with this `commit_token` is still running.
- `422`: The values did not validate; `error.details` says which

---

[API](https://skmtc.dev/podpdf/apis/podpdf-api.md) · [All operations](https://skmtc.dev/podpdf/apis/podpdf-api/llms.txt) · [OpenAPI document](https://skmtc.dev/podpdf/apis/podpdf-api/revisions/84eb21f3ee27?raw)
