Every stage PDF carries its own metadata for indexing. The same values are written twice, once as an XMP packet and once as PDF DocInfo keys, so you can read whichever your toolchain handles more comfortably.
| XMP key | DocInfo | Type | Value |
|---|---|---|---|
| dc:title | Title | text | Stage name |
| dc:creator | Author | text | Designer name. Falls back to the account display name when the designer has not credited themselves |
| dc:description | Subject | text | One-line summary, e.g. IDPA, 12 targets, 3 steel, 24 rounds, concealment |
| dc:identifier | not set | text | Same value as ppsf:pdfUrl |
XMP serializes everything as text. The type column is how to parse it.
| XMP key | DocInfo | Type | Value |
|---|---|---|---|
| ppsf:stageId | PPSF-StageId | text | 128 hex characters, stable for the life of the stage. Use it to recognise a stage you already hold |
| ppsf:title | PPSF-Title | text | Stage name. Same value as dc:title, repeated so the full field set sits in one namespace |
| ppsf:sourceVersion | PPSF-SourceVersion | text | Release of Pew Pew Stage Factory that wrote the stage, e.g. 0.9.0 |
| ppsf:ruleset | PPSF-Ruleset | text | GPA IDPA USPSA PCSL ICORE IPSC Other |
| ppsf:scoring | PPSF-Scoring | text | unlimited limited comstock |
| ppsf:targets | PPSF-Targets | integer | Paper target count |
| ppsf:nonThreats | PPSF-NonThreats | integer | No-shoot target count. A real 0 means the stage puts up none; the field is absent when it could not be determined |
| ppsf:steel | PPSF-Steel | integer | Steel target count |
| ppsf:rounds | PPSF-Rounds | integer | Minimum round count |
| ppsf:shotsPerTarget | PPSF-ShotsPerTarget | integer | Required shots per paper target |
| ppsf:strings | PPSF-Strings | integer | String count, 0 for a single-string stage |
| ppsf:concealment | PPSF-Concealment | boolean | true or false |
| ppsf:parSeconds | PPSF-ParSeconds | decimal | Par time in seconds, e.g. 13.59. Absent when the stage has no par time |
| ppsf:bayWidthYards | PPSF-BayWidthYards | decimal | Bay width across the firing line, in yards |
| ppsf:bayLengthYards | PPSF-BayLengthYards | decimal | Bay length downrange, in yards |
| ppsf:distanceMinYards | PPSF-DistanceMinYards | decimal | Nearest engageable target, in yards. Absent when the stage has no shooting positions to measure from |
| ppsf:distanceMaxYards | PPSF-DistanceMaxYards | decimal | Farthest engageable target, in yards. Absent on the same terms |
| ppsf:impactArea | PPSF-ImpactArea | text | downrange or side. Read the note below before filtering on it |
| ppsf:impactAreaSource | PPSF-ImpactAreaSource | text | computed declared default |
| ppsf:designer | PPSF-Designer | text | Same value as dc:creator, repeated so the full filter set sits in one namespace |
| ppsf:is3d | PPSF-Is3d | boolean | true for stages designed in 3D |
| ppsf:modified | PPSF-Modified | text | ISO 8601 UTC, e.g. 2026-09-10T14:22:07Z |
| ppsf:diagramUrl | PPSF-DiagramUrl | text | Link to the stage diagram on its own, without the sheet around it. Public, no login |
| ppsf:diagramFormat | PPSF-DiagramFormat | text | jpg png or svg, so you know what ppsf:diagramUrl serves before you fetch it |
| ppsf:pdfUrl | PPSF-PdfUrl | text | Link to this PDF. Public, no login |
| ppsf:stageUrl | PPSF-StageUrl | text | Link to the stage page: preview, and an option to copy it into an account for editing. Public, no login to view |
| ppsf:startPosition | PPSF-StartPosition | text | Where the shooter starts, as written by the designer |
| ppsf:scenario | PPSF-Scenario | text | The scenario description. Absent on stages that do not have one |
| ppsf:procedure | PPSF-Procedure | text | The stage procedure, as written by the designer |
| ppsf:firearmCondition | PPSF-FirearmCondition | text | How the firearm starts, e.g. loaded and holstered |
| ppsf:targetDesignations | PPSF-TargetDesignations | text | How the targets are labelled on the diagram, e.g. T1-T8 threats. NT1-NT2 no-shoots. |
| ppsf:specialConditions | PPSF-SpecialConditions | text | Anything unusual about the stage: low ports, restricted movement |
| ppsf:muzzleSafe | PPSF-MuzzleSafe | text | Muzzle safe points and direction limits |
| ppsf:steelMustFall | PPSF-SteelMustFall | boolean | true or false |
| ppsf:mandatoryReloadWaived | PPSF-MandatoryReloadWaived | boolean | true or false |
| ppsf:notes | PPSF-Notes | text | The designer's own notes. Absent on stages that do not have any |
downrange means every shot in the stage is taken into the downrange backstop.
side means the stage may put rounds into a side boundary. Read it as may, not does: the field is deliberately conservative, and any stage we cannot positively confirm as downrange-only reports side.
So downrange is a claim, and side is the absence of one. Filtering for downrange-only stages will be accurate. Filtering for stages that definitely involve side impacts will not.
ppsf:impactAreaSource tells you where the value came from, if you want to weight it:
computed is our own assessment of the stage design.declared is the designer stating it themselves, which overrides ours.default means there was not enough in the design to assess, so the value fell back to side.ppsf:startPosition, ppsf:scenario and ppsf:procedure carry the designer's own wording, exactly as it appears on the page.
One difference worth knowing if you display them: they arrive as a single line. Line breaks are collapsed to spaces, because a metadata value is single-line by nature. No words are lost, but the designer's paragraph breaks are not in there, so do not expect them to lay out the way they do on the PDF page itself.
ppsf:scenario is absent on stages that do not have one.
Par is specific to certain rulesets. Where it does not apply the field is absent rather than zero.
For a stage designed in 3D they describe the bay the designer selected. For a 2D stage they describe the drawing area. Both are in yards; ppsf:is3d tells you which you are holding.
ppsf:diagramUrl serves the stage drawing on its own, not a page of the PDF. A stage designed in 3D serves a raster of its printed diagram; a 2D stage serves SVG. Check ppsf:diagramFormat before fetching.
ppsf:firearmCondition, targetDesignations, specialConditions, muzzleSafe, steelMustFall and mandatoryReloadWaived are authored only on stages whose ruleset uses them, which today means GPA. They are absent everywhere else, so treat a missing one as "not applicable to this stage" rather than "the designer left it blank".
Each target is measured to the nearest shooting position, and the two fields are the spread across the stage. They cover paper and steel alike. Both are absent, rather than zero, on a stage whose designer placed no shooting positions.
Neither is a snapshot. A stage edited after you index it will serve different content at the same URL, so compare ppsf:modified against what you stored to spot a revision.
Trimmed to a few fields. The full packet is a standard XMP wrapper.
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:ppsf="urn:ppsf:stage:1">
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Cold Open</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>R. Staph</rdf:li></rdf:Seq></dc:creator>
<ppsf:stageId>3f9c1a...</ppsf:stageId>
<ppsf:ruleset>GPA</ppsf:ruleset>
<ppsf:rounds>18</ppsf:rounds>
<ppsf:nonThreats>3</ppsf:nonThreats>
<ppsf:distanceMinYards>3.8</ppsf:distanceMinYards>
<ppsf:distanceMaxYards>16.7</ppsf:distanceMaxYards>
<ppsf:diagramUrl>https://www.pewpewstagefactory.com/stagefiles/...</ppsf:diagramUrl>
<ppsf:diagramFormat>jpg</ppsf:diagramFormat>
<ppsf:bayWidthYards>9.3</ppsf:bayWidthYards>
<ppsf:bayLengthYards>16.7</ppsf:bayLengthYards>
<ppsf:impactArea>side</ppsf:impactArea>
<ppsf:impactAreaSource>computed</ppsf:impactAreaSource>
<ppsf:modified>2026-09-10T14:22:07Z</ppsf:modified>
</rdf:Description>
The version is carried at the end of the namespace so you can pin against it.
New fields are additive and stay in urn:ppsf:stage:1. Read the keys you know and ignore the rest, as you would with any other metadata namespace, and an addition cannot break you. urn:ppsf:stage:2 is reserved for a change that would: renaming a key, repurposing one, or changing the units or value set of a key you already read.
Fields that could not be determined for a stage are absent rather than empty or zero, so a key you find always carries a real value.
| 2026-09-22 | Added title, sourceVersion, nonThreats, shotsPerTarget, distanceMinYards, distanceMaxYards, diagramUrl, diagramFormat, notes, and the six ruleset-specific fields firearmCondition, targetDesignations, specialConditions, muzzleSafe, steelMustFall, mandatoryReloadWaived. |
| 2026-09-10 | First published set. |