EmbedPDF

Metadata

A PDF carries information about itself: a title, an author, when it was made. PDF apps show it in their document properties.

const metadata = await doc.metadata.get();
console.log(metadata.title, metadata.author);
 
await doc.metadata.update({ title: 'Q2 Proposal (final)' });

Read#

get() returns every standard field. A field the file doesn’t set is null:

FieldWhat it is
titleThe document’s title.
authorWho wrote it.
subjectWhat it’s about.
keywordsKeywords, as one string (often separated by commas).
creatorThe app the document was made in, such as a word processor.
producerThe app that turned it into a PDF.
createdAtWhen it was created, as an ISO date.
modifiedAtWhen it was last changed, as an ISO date.
trappedA print-shop setting: 'true', 'false' or 'unknown'.

Keys of your own, and those other apps added, are read separately: see Your own keys.

Change#

update() changes only the fields you pass. null removes a field:

const { metadata } = await doc.metadata.update({
  title: 'Q2 Proposal (final)',
  author: 'Dana Smith',
  subject: null, // remove the subject
});

It returns the metadata as it is after the change, the same shape get() returns. What you read goes back in as it is, so “change a copy, then save it” works:

const current = await doc.metadata.get();
await doc.metadata.update({ ...current, keywords: `${current.keywords ?? ''}, approved` });

An empty string is a value, not a removal: title: '' sets an empty title.

Dates#

Dates are ISO strings, with the time zone offset the PDF gives:

metadata.createdAt; // '2017-07-12T21:44:38-07:00'

They’re strings, not Date objects, so a date survives JSON and goes back into update() as it is. new Date(metadata.createdAt) turns one into a Date when you need to compute with it.

When you write a date, pass an ISO string or a Date. A string keeps its offset; a Date is written in UTC:

await doc.metadata.update({
  createdAt: '2026-01-04T09:00:00+01:00',
  modifiedAt: new Date(),
});

PDF dates have whole seconds, so milliseconds are dropped. A string that isn’t an ISO date and time fails with InvalidArg.

Your own keys#

Store your own values in the document: a contract number, who reviewed it. Other apps add keys like these too, and PDF apps show them as custom properties. They have their own object, doc.metadata.custom, with the same two calls:

const custom = await doc.metadata.custom.get();
// { reviewedBy: 'dana', draftOwner: 'sam' }, or {} when there are none

update() works like it does for the standard fields, with each key as a field: a string sets the key, null removes it, and keys you leave out stay as they are.

const { custom } = await doc.metadata.custom.update({
  reviewedBy: 'lee',
  contractId: 'C-2026-114',
  draftOwner: null, // remove this key
});

It returns the keys as they are after the change, the same shape get() returns.

A key is 1 to 127 printable ASCII characters, doesn’t start with /, and isn’t the name of a standard field (Title, Author, CreationDate, …): use doc.metadata.update() for those. An invalid key fails with InvalidArg, which names the key, and nothing is written.

Permissions#

Reading metadata needs doc.open. Changing it, your own keys included, needs doc.metadata.modify; without it, update() fails with Forbidden.

Good to know#

The modified date#

The engine doesn’t change modifiedAt for you, neither when you update metadata nor when you save. Set it yourself when your app wants it to say when the document last changed:

await doc.metadata.update({ modifiedAt: new Date() });

Where it’s stored#

The fields are the PDF’s document information (the /Info dictionary, with keys like /Title and /CreationDate). Your own keys live in the same dictionary under their own names: reviewedBy is /reviewedBy. trapped is 'unknown' both when the file says so and when it doesn’t say.

The XMP copy#

Many PDFs carry their metadata a second time, as XML in an XMP packet, and some PDF apps read that copy first. update() keeps it in step: a field you set is written there too, and a field you set to null is removed from it. A removed author or app name is gone from both places.

FieldXMP property
titledc:title
authordc:creator
subjectdc:description
keywordspdf:Keywords, and the dc:subject list when the file has one
creatorxmp:CreatorTool
producerpdf:Producer
createdAtxmp:CreateDate
modifiedAtxmp:ModifyDate
trappedpdf:Trapped
  • A field you set is added to the XMP when it isn’t there yet. PDF/A asks the two copies to match.
  • Your own keys follow the pdfx: properties Adobe apps copy them to: null removes one, and a value changes one that’s there. None is added, because PDF/A files would then need a schema for it.
  • Everything else in the XMP stays exactly as it was: a PDF/A identification, other apps’ properties, an editing history. That history can name the apps that edited the file.
  • A file without XMP gets none, and XMP that isn’t valid XML is left as it is.

get() reads /Info only. A field that’s only in the XMP still reads null, and update({ author: null }) removes it from the XMP all the same.

An incremental save appends your changes, so the file still holds the earlier values. To share a file without them, save it with download({ mode: 'rewrite' }). See Saving.

Why your own keys are separate#

update() keeps what you leave out one level deep: each field you pass replaces that field. Your own keys are fields too, so they get an object of their own, where each key is one of those fields. Adding a key never touches the others, and a key can’t be mistaken for a standard field.

Other code can follow changes with the metadata.updated and metadata.customUpdated events. See Events.

Was this page helpful?

Your feedback goes directly to the documentation team.