Blog/PDF tools

Watermark a PDF with a watermark pdf api

PDF tools··7 min read

To watermark a PDF with a watermark pdf api, POST an existing PDF to /v1/pdf/stamp and send exactly one of text or image. The product page names this as a watermark for draft marks, confidential labels, or a logo. Position defaults to center. This is a PDF tool, not a renderer: you send a finished file (url or file), not HTML. Product: https://relaypdf.com/pdf-tools/stamp. Hub: https://relaypdf.com/pdf-tools. REST list: https://relaypdf.com/docs.

Typical order is generate or convert first, then stamp. Print a report with POST /v1/pdf, convert Office with POST /v1/convert, merge a packet with POST /v1/pdf/merge if several files must be one, then stamp the bytes. Each successful tools call is $0.005 on the live rate card. Failed jobs are not billed. This article uses only documented fields.

Text stamp vs image stamp

OpenAPI names the body StampPdfRequest. The stamp product page is explicit: exactly one of text or image. text is a string. image is a FileSource: url (URI) and/or file (base64 payload, optional data-URI prefix). Sending both text and image is not documented. Sending neither is not documented. The docs hub row is “Text or image watermark.” There is no second stamp endpoint and no field named watermark.

Use text for status words such as DRAFT or CONFIDENTIAL. The product JSON example is url plus text DRAFT plus rotate -24. Use image for a logo or other graphic you already host or hold as bytes. Barcodes are a different product: POST /v1/barcodes returns PNG or SVG. That endpoint does not stamp onto a PDF. If you need a QR on a page, generate the asset first, then send it as image on this endpoint.

The source PDF is url or file on the same object. password is an optional string on StampPdfRequest (same name as other tool sources). filename, response, and callbackUrl are the shared output fields. Mixing html, markdown, or templateId into this body is not documented.

You haveSend
A finished PDF to markPOST /v1/pdf/stamp
Status word (DRAFT / CONFIDENTIAL)text (exactly one of text or image)
Logo or other graphicimage.url or image.file
A public HTTPS PDFurl
PDF bytes you already holdfile (base64 in JSON; SDKs accept bytes)
A passworded source PDFpassword (optional string)
A Word or HTML sourceConvert or render first, then stamp
A QR or barcode imagePOST /v1/barcodes first, then image

Documented placement and look

position is either a named enum or an object with x and y numbers. Enum values in OpenAPI: center, top, bottom, top-left, top-right, bottom-left, bottom-right. The product page says position defaults to center. The object form is { x, y }. Units, origin, and coordinate space are not published; do not invent them in client copy.

Optional look fields on StampPdfRequest: opacity (number), fontSize (number), color (string), rotate (number), scale (number). The live product sample sets rotate to -24 with text DRAFT. Python README uses the same rotate=-24. OpenAPI does not publish ranges, units, or a color grammar. Do not send font family names, alignment keys, or layer flags that are not on this schema.

pages is optional. Type is a string or an array of integers. Other tools on the same API use 1-indexed ranges in string form (CLI extract uses --pages 1-3). Stamp does not republish a default. If you need a subset, send pages. If you omit it, do not claim a default the schema does not state.

Limits and price (live, 23 Aug 2026 ET)

Stamp is billed as a PDF tool. Pricing lists “PDF tools /v1/pdf/* · /v1/barcodes · /v1/zip” at $0.005 per successful operation. Wallet and SDK READMEs name stamp in that tools list at the same rate. Failed jobs, validation errors, 429s, and 402s do not debit.

A megabyte cap is not published on pricing, the docs hub, the stamp product page, or OpenAPI. Oversized HTML or files return HTTP 413 payload_too_large. New accounts get the $5 trial. Trial rate limit is 20/min; funded or auto-reload is 60/min; burst is 5 / 10s (SDK READMEs). callbackUrl must be https. Private, loopback, and metadata hosts on url are url_not_allowed.

ConstraintPublished valueSource
EndpointPOST /v1/pdf/stampdocs hub; OpenAPI
Exclusive markexactly one of text or imageproduct page
textstringStampPdfRequest
imageFileSource (url and/or file)StampPdfRequest
Source PDFurl or file; optional passwordStampPdfRequest
position defaultcenterproduct page
position enumcenter, top, bottom, top-left, top-right, bottom-left, bottom-rightOpenAPI
position object{ x, y } numbersOpenAPI
Look fieldsopacity, fontSize, color, rotate, scaleStampPdfRequest
pagesstring or integer arrayStampPdfRequest
Price$0.005 per successful job/pricing
Output modesbinary | url | asyncdocs hub; ResponseMode
url-mode downloadGET /v1/files/:id, 24 hours, no keydocs hub
Byte capNot published; 413 payload_too_large/docs/errors

Stamp DRAFT or CONFIDENTIAL

Send one source plus text. filename names the output. Default response is binary: PDF bytes plus x-relaypdf-id, x-relaypdf-size, and content-disposition. Set response to url for a 24-hour GET /v1/files/:id with no key. Set response to async for HTTP 202 and poll GET /v1/jobs/:id, or supply callbackUrl (https).

curl -X POST https://api.relaypdf.com/v1/pdf/stamp
  -H "Authorization: Bearer $RELAYPDF_API_KEY"
  -H "Content-Type: application/json"
  -d '{
    "url": "https://example.com/report.pdf",
    "text": "DRAFT",
    "rotate": -24,
    "filename": "report-draft.pdf"
  }'
  --output report-draft.pdf

That JSON matches the product page (url, text DRAFT, rotate -24) plus filename. Swap the string for CONFIDENTIAL when that is the label you need. Named position if you do not want the documented center default:

curl -X POST https://api.relaypdf.com/v1/pdf/stamp
  -H "Authorization: Bearer $RELAYPDF_API_KEY"
  -H "Content-Type: application/json"
  -d '{
    "url": "https://example.com/report.pdf",
    "text": "CONFIDENTIAL",
    "position": "top-right",
    "opacity": 0.35,
    "fontSize": 18,
    "color": "#cc0000",
    "filename": "report-confidential.pdf"
  }'
  --output report-confidential.pdf

opacity, fontSize, and color are documented types only (number, number, string). The hex in the sample is an illustrative string, not a published color grammar. If a request is rejected, treat it as invalid_request and check the message.

Stamp an image

Omit text. Set image to a FileSource. Prefer image.url when the graphic is already on a public HTTPS host so the body stays small. Prefer image.file when you hold bytes (SDK file may be bytes). scale is the documented size control for the mark; there is no width or height field on StampPdfRequest.

curl -X POST https://api.relaypdf.com/v1/pdf/stamp
  -H "Authorization: Bearer $RELAYPDF_API_KEY"
  -H "Content-Type: application/json"
  -d '{
    "url": "https://example.com/report.pdf",
    "image": { "url": "https://example.com/logo.png" },
    "position": "bottom-right",
    "scale": 0.4,
    "opacity": 0.5,
    "filename": "report-branded.pdf"
  }'
  --output report-branded.pdf

Node, Python, and CLI

Python (relaypdf 0.1.x) exposes pdf.stamp(**input). REST field names stay camelCase. file may be bytes. The published example stamps merge output: client.pdf.stamp(file=pack.bytes, text="DRAFT", rotate=-24). Node maps the same REST body on the pdf tools surface. CLI on /docs/cli: relaypdf stamp in.pdf --text DRAFT --out stamped.pdf. MCP lists stamp. Do not ask anyone to paste a key; run npx @relaypdf/cli setup.

import os
from relaypdf import RelayPDF
client = RelayPDF(api_key=os.environ["RELAYPDF_API_KEY"])
pack = client.pdf.merge(
    files=[
        {"url": "https://example.com/cover.pdf"},
        {"url": "https://example.com/body.pdf"},
    ]
)
stamped = client.pdf.stamp(
    file=pack.bytes,
    text="DRAFT",
    rotate=-24,
    filename="pack-draft.pdf",
)
stamped.save("pack-draft.pdf")
import { RelayPDF } from "@relaypdf/sdk";
const client = new RelayPDF({
  apiKey: process.env.RELAYPDF_API_KEY!,
});
const report = await client.pdf.fromUrl("https://example.com/report.html", {
  filename: "report.pdf",
});
const stamped = await client.pdf.stamp({
  file: report.kind === "binary" ? report.bytes : undefined,
  text: "CONFIDENTIAL",
  position: "center",
  rotate: -24,
  filename: "report-confidential.pdf",
});
await stamped.save("report-confidential.pdf");

Errors and what is not billed

Failures are { error: { code, message } }. SDKs throw RelayPDFError. Branch on code. invalid_request covers a body that is not exactly one of text or image, or otherwise invalid fields. url_not_allowed covers a private, loopback, or metadata host, or a non-https callbackUrl. payload_too_large is 413. processing_failed is a 502 on a tool job. payment_required is an empty wallet. rate_limited includes Retry-After. None of those debit.

Only a successful stamp (HTTP 200, or a completed async job) takes $0.005. Rendering first is $0.015. A LibreOffice convert first is $0.04. Merge, protect, extract, compress, info, and zip are separate tools jobs if you call them.

What this is not

Stamp does not render HTML, convert Office, merge, protect, extract, or zip. Chain those documented tools on the bytes or on the 24-hour url. Sibling how-to: password-protect (https://relaypdf.com/pdf-tools/protect). Invoice or report layout belongs on POST /v1/pdf. POST /v1/barcodes does not apply a mark to an existing PDF.

The public stamp schema has no font-family list, no alignment object beyond position, no page-box names, and no published opacity or scale range. Do not invent them. A watermark is not access control. url-mode downloads are public for 24 hours with no key. If the document must stay closed, protect it after you stamp (userPassword on POST /v1/pdf/protect).

Ship it

Use the watermark pdf api when the source is already a PDF and you need a text or image mark. Exclusive: text or image. Position defaults to center. $0.005 on success. Start at https://relaypdf.com/pdf-tools/stamp. Copy StampPdfRequest from https://relaypdf.com/openapi.json. Auth is a bearer key on https://api.relaypdf.com.

Ready to generate?

One API for HTML, Markdown, URLs, and Office. REST, SDK, CLI, or MCP.