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 have | Send |
|---|---|
| A finished PDF to mark | POST /v1/pdf/stamp |
| Status word (DRAFT / CONFIDENTIAL) | text (exactly one of text or image) |
| Logo or other graphic | image.url or image.file |
| A public HTTPS PDF | url |
| PDF bytes you already hold | file (base64 in JSON; SDKs accept bytes) |
| A passworded source PDF | password (optional string) |
| A Word or HTML source | Convert or render first, then stamp |
| A QR or barcode image | POST /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.
| Constraint | Published value | Source |
|---|---|---|
| Endpoint | POST /v1/pdf/stamp | docs hub; OpenAPI |
| Exclusive mark | exactly one of text or image | product page |
| text | string | StampPdfRequest |
| image | FileSource (url and/or file) | StampPdfRequest |
| Source PDF | url or file; optional password | StampPdfRequest |
| position default | center | product page |
| position enum | center, top, bottom, top-left, top-right, bottom-left, bottom-right | OpenAPI |
| position object | { x, y } numbers | OpenAPI |
| Look fields | opacity, fontSize, color, rotate, scale | StampPdfRequest |
| pages | string or integer array | StampPdfRequest |
| Price | $0.005 per successful job | /pricing |
| Output modes | binary | url | async | docs hub; ResponseMode |
| url-mode download | GET /v1/files/:id, 24 hours, no key | docs hub |
| Byte cap | Not 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.