CloudPDF
DocsPricing
Start building

Annotation types

This page lists every annotation you can create with page.annotations.create(...) and what you pass for each. For reading, updating, styling, and flags, see Annotations.

Coordinates are in PDF points with the origin at the bottom-left of the page (y goes up). A rectangle is { left, bottom, right, top } and a point is { x, y }.

Every annotation also accepts these optional base fields, so they’re left out of the per-type tables below:

  • contents — a text note/comment.
  • flags — PDF flags, e.g. { print: true } (see Annotations).
  • nm — a stable name you want to assign.

Anything you omit falls back to its default (a 1pt solid red stroke, full opacity, no fill).

Any annotation kind the engine doesn’t author yet is read back as unsupported. It’s listed and preserved, but can’t be created or have its type-specific fields edited.

Text markup#

highlight, underline, squiggly, and strikeout all work the same way: a color and one or more quadPoints quads marking the text regions. A quad is four points (p1p4).

await page.annotations.create({
  subtype: 'highlight', // or 'underline' | 'squiggly' | 'strikeout'
  color: { r: 255, g: 215, b: 0 },
  opacity: 0.4,
  quadPoints: [
    { p1: { x: 72, y: 712 }, p2: { x: 272, y: 712 }, p3: { x: 72, y: 696 }, p4: { x: 272, y: 696 } },
  ],
});
FieldWhat it isRequiredDefault
quadPointsOne quad per marked text regionyes
colorMarkup color { r, g, b }nored
opacityTransparency 01no1

Square and circle#

square (rectangle) and circle (ellipse) are drawn inside a rect, with a stroke (color) and an optional fill (interiorColor).

await page.annotations.create({
  subtype: 'circle', // or 'square'
  rect: { left: 60, bottom: 300, right: 180, top: 400 },
  color: { r: 0, g: 0, b: 139 },
  interiorColor: { r: 30, g: 144, b: 255 },
  strokeWidth: 2,
  borderStyle: 'solid',
  opacity: 0.5,
});
FieldWhat it isRequiredDefault
rectBounding box { left, bottom, right, top }yes
colorStroke colornored
interiorColorFill color, or null for nonenonone
strokeWidthBorder thicknessno1
borderStyle'solid'/'dashed'/'beveled'/'inset'no'solid'
dashArrayDash pattern (with 'dashed')no
opacityTransparency 01no1
cloudyIntensityCloudy-border strength (0 = plain)no0
rectDifferencesInset of the shape from rect (see below)nonone

Polygon and polyline#

Both take a vertices point list and a rect. A polygon is closed and can be filled; a polyline is open and can have arrowheads (lineEndings).

// Polygon (closed, filled)
await page.annotations.create({
  subtype: 'polygon',
  rect: { left: 60, bottom: 450, right: 180, top: 550 },
  vertices: [
    { x: 70, y: 460 },
    { x: 170, y: 460 },
    { x: 120, y: 540 },
  ],
  color: { r: 0, g: 0, b: 139 },
  interiorColor: { r: 255, g: 215, b: 0 },
  strokeWidth: 2,
});
 
// Polyline (open, with arrowheads)
await page.annotations.create({
  subtype: 'polyline',
  rect: { left: 220, bottom: 450, right: 360, top: 550 },
  vertices: [
    { x: 230, y: 460 },
    { x: 290, y: 540 },
    { x: 350, y: 460 },
  ],
  color: { r: 220, g: 20, b: 60 },
  strokeWidth: 2,
  lineEndings: { start: 'open-arrow', end: 'closed-arrow' },
});
FieldWhat it isRequiredDefault
verticesOrdered points { x, y }yes
rectBounding boxyes
colorStroke colornored
interiorColorFill, or null (polygon)nonone
strokeWidthLine thicknessno1
borderStyle'solid'/'dashed'/'beveled'/'inset'no'solid'
dashArrayDash pattern (with 'dashed')no
opacityTransparency 01no1
lineEndingsArrowheads — polyline onlynonone
cloudyIntensityCloudy border — polygon onlyno0

Line#

A single straight line between two points, with optional endings.

await page.annotations.create({
  subtype: 'line',
  rect: { left: 400, bottom: 450, right: 520, top: 550 },
  linePoints: { start: { x: 410, y: 460 }, end: { x: 510, y: 540 } },
  color: { r: 0, g: 128, b: 128 },
  strokeWidth: 2,
  lineEndings: { start: 'none', end: 'open-arrow' },
});
FieldWhat it isRequiredDefault
linePoints{ start: { x, y }, end: { x, y } }yes
rectBounding boxyes
colorStroke colornored
strokeWidthLine thicknessno1
borderStyle'solid'/'dashed'/'beveled'/'inset'no'solid'
dashArrayDash pattern (with 'dashed')no
opacityTransparency 01no1
lineEndingsEndings at start/endnonone

Ink#

Freehand drawing. inkList is an array of strokes; each stroke is an array of points (one pen path). Ink has a stroke but no fill.

await page.annotations.create({
  subtype: 'ink',
  rect: { left: 60, bottom: 60, right: 300, top: 200 },
  inkList: [
    [ { x: 70, y: 80 }, { x: 120, y: 180 }, { x: 180, y: 90 } ], // stroke 1
    [ { x: 200, y: 100 }, { x: 260, y: 160 } ],                  // stroke 2
  ],
  color: { r: 220, g: 20, b: 60 },
  strokeWidth: 3,
});
FieldWhat it isRequiredDefault
inkListArray of strokes; each an array of pointsyes
rectBox covering all strokesyes
colorStroke colornored
strokeWidthPen thicknessno1
borderStyle'solid'/'dashed'/'beveled'/'inset'no'solid'
dashArrayDash pattern (with 'dashed')no
opacityTransparency 01no1

Free text and callout#

A free-text annotation draws text directly on the page (a sticky label, a comment box, or a callout that points at something). It’s one type with two intent values: 'free-text' for a plain box and 'free-text-callout' for a box with a leader line.

The colors are the part worth understanding:

  • color — the border color and the text color. This is the one color you almost always set.
  • fontColor — optional. Set it only when you want the text a different color than the border. Leave it out and the text just follows color.
  • interiorColor — the box background. Leave it out (or pass null) for a transparent box.
// Plain text box
await page.annotations.create({
  subtype: 'free-text',
  intent: 'free-text',
  rect: { left: 60, bottom: 600, right: 260, top: 660 },
  fontFamily: 'helvetica',
  fontSize: 14,
  textAlign: 'center',
  contents: 'Please review this section',
  color: { r: 20, g: 40, b: 60 },        // border + text
  interiorColor: { r: 250, g: 250, b: 210 }, // pale yellow background
});
 
// Callout (box + leader line pointing at something)
await page.annotations.create({
  subtype: 'free-text',
  intent: 'free-text-callout',
  rect: { left: 280, bottom: 600, right: 480, top: 660 },
  fontFamily: 'times-roman',
  fontSize: 12,
  textAlign: 'left',
  contents: 'Look here',
  color: { r: 0, g: 0, b: 0 },
  fontColor: { r: 200, g: 0, b: 0 }, // red text, black border
  calloutLine: [
    { x: 265, y: 605 }, // the point being called out
    { x: 320, y: 630 }, // optional knee
    { x: 280, y: 640 }, // where the line meets the box
  ],
  lineEnding: 'open-arrow',
});
FieldWhat it isRequiredDefault
intent'free-text' or 'free-text-callout'yes
rectBounding boxyes
fontFamilyA standard font or a registered font key (see below)yes
fontSizeText size in pointsyes
textAlign'left'/'center'/'right'yes
contentsThe text to showno
colorBorder + default text colornoblack
fontColorText color overridenofollows color
interiorColorBox background, or null for nonenonone
opacityTransparency 01no1
strokeWidthBorder thicknessno1
borderStyle'solid'/'dashed'/'beveled'/'inset'no'solid'
dashArrayDash pattern (with 'dashed')no
calloutLineLeader line, 2 or 3 points — callout onlynonone
lineEndingArrowhead at the called-out end — callout onlynonone
rectDifferencesInset of the text box from rectnonone

The calloutLine is 2 points for a straight leader or 3 for a knee-jointed one. The first point is what’s being pointed at; the last point touches the text box. lineEnding is the arrowhead drawn at the pointed-at end — same names as line endings.

Fonts#

fontFamily is one of the 14 standard PDF fonts, so they render everywhere without embedding:

courier, courier-bold, courier-bold-oblique, courier-oblique, helvetica, helvetica-bold, helvetica-bold-oblique, helvetica-oblique, times-roman, times-bold, times-bold-italic, times-italic, symbol, zapf-dingbats.

fontFamily also accepts the key of a font you’ve registered with the engine — pass 'noto-sc' instead of a standard name to draw CJK, Cyrillic, or any other script, and a glyph subset is embedded on save. See Custom fonts.

Vertical alignment and rich text aren’t authored yet — text is single-style and top-aligned in the box.

Caret#

A caret marks a place in the text — typically where something should be inserted or where an edit was made. It’s the simplest annotation: just a position (rect), a color, and an opacity.

await page.annotations.create({
  subtype: 'caret',
  rect: { left: 50, bottom: 700, right: 90, top: 730 },
  color: { r: 0, g: 0, b: 255 },
});
FieldWhat it isRequiredDefault
rectWhere the caret sitsyes
colorCaret colornored
opacityTransparency 01no1
rectDifferencesInset of the drawn caret from rectnonone

Stamp#

A stamp places an image — or a vector appearance — inside rect. You pass the bytes inline as source; there is no separate upload or attachment step. The format is detected from the bytes themselves (never from a declared mime type), and each format takes the best path into the PDF:

  • PNG — decoded natively, transparency preserved.
  • JPEG — embedded as-is (no re-encoding).
  • Single-page PDF — cloned in as a vector appearance that stays crisp at any zoom.
// From a file input, a fetch, or raw bytes — Blob and Uint8Array both work.
await page.annotations.create({
  subtype: 'stamp',
  rect: { left: 72, bottom: 640, right: 222, top: 715 },
  source: file, // PNG, JPEG, or single-page PDF bytes
  fit: 'contain',
});
FieldWhat it isRequiredDefault
rectBounding boxyes
sourceThe content bytes: Blob, Uint8Array, or { data, mimeType?, name? }yes
fitHow the content maps into rect (see below)no'contain'
nameStandard stamp label ('Approved', 'Draft', 'Confidential', …)nonone

fit uses the CSS object-fit vocabulary: 'contain' preserves the aspect ratio and keeps the content fully visible, 'cover' preserves the aspect ratio and fills the box (may crop), 'fill' stretches to the box.

The bytes are written into the stamp’s appearance stream, so the document stays fully self-contained — it renders in any PDF viewer and survives download and re-open with no side-car storage. The call is identical on the local and cloud engines.

Changing the geometry later (an update with a new rect, or a resize in the viewer) re-fits the existing appearance — the image is never re-uploaded or re-encoded. Sending a new source in an update replaces the content:

await page.annotations.update(ref, { subtype: 'stamp', source: otherImage });

Unsupported bytes (anything that isn’t PNG, JPEG, or PDF) reject with InvalidArg before any work happens.

Cloudy borders#

square, circle, and polygon support a cloudy (scalloped) border instead of a straight one — the wavy outline reviewers often use to circle a region. Turn it on with cloudyIntensity, a number that sets how pronounced the waves are:

  • 0 (or omitted) — a plain, straight border.
  • around 1 — gentle waves.
  • around 2 — larger, more pronounced waves.
await page.annotations.create({
  subtype: 'square',
  rect: { left: 60, bottom: 300, right: 180, top: 400 },
  color: { r: 220, g: 20, b: 60 },
  strokeWidth: 2,
  cloudyIntensity: 2, // wavy "cloud" outline
});

Higher values mean bigger scallops. cloudyIntensity only affects the outline; the fill (interiorColor) and everything else behave the same. It has no effect on line, polyline, or text markup.

Rectangle differences#

square, circle, free-text, and caret accept an optional rectDifferences — the gap, in points, between the rect you pass and where the shape (or text box) is actually drawn. It’s four non-negative insets, one per edge:

rectDifferences: { left: 6, top: 6, right: 6, bottom: 6 } // drawn 6pt inside rect on every side

You mostly need this with a cloudy border: the scallops bulge outward, so rect has to be a little larger than the shape to fully contain them. rectDifferences records how far the shape sits inside rect so the bounding box stays correct. For a plain border you can leave it out — the shape just fills rect.

polygon doesn’t use rectDifferences: its outline is already given exactly by vertices (plus rect), so there’s nothing to inset.

Line endings#

For line and polyline, lineEndings is { start, end } where each is one of:

none, square, circle, diamond, open-arrow, closed-arrow, butt, r-open-arrow, r-closed-arrow, slash.

The r- variants are reversed. Omit lineEndings to leave both ends plain.