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:
| Field | What it is |
|---|---|
title | The document’s title. |
author | Who wrote it. |
subject | What it’s about. |
keywords | Keywords, as one string (often separated by commas). |
creator | The app the document was made in, such as a word processor. |
producer | The app that turned it into a PDF. |
createdAt | When it was created, as an ISO date. |
modifiedAt | When it was last changed, as an ISO date. |
trapped | A 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 noneupdate() 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.
| Field | XMP property |
|---|---|
title | dc:title |
author | dc:creator |
subject | dc:description |
keywords | pdf:Keywords, and the dc:subject list when the file has one |
creator | xmp:CreatorTool |
producer | pdf:Producer |
createdAt | xmp:CreateDate |
modifiedAt | xmp:ModifyDate |
trapped | pdf: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:nullremoves 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.
Your feedback goes directly to the documentation team.