Generating Label PDFs¶
Introduction¶
T3 label PDFs are generated using several different sources that control how the labels will appear:
• Templates
• Layouts
• Data List
• Default Data
• Built-In Data
• Images
• Settings
Templates¶
A template defines the overall layout and physical structure of the label, including its dimensions, margins, padding, and page arrangement such as rows, columns, and spacing.
Learn more about label templates
Layouts¶
Layouts control what will be printed on each label and how it is arranged. When you are configuring a label, this is where most of the effort will be dedicated.
Learn more about layouts and layout elements
Data List¶
The data list controls how many unique labels are generated, and what data is inserted into each label.
Each item in the data list represents one label. If you wanted to print a set of 5 labels that didn't need any special data into each, you would just create an empty list of length 5:
If you wanted to insert two values, say firstName and lastName into each label, you could specify that as follows:
Package data returned from the T3 API is structured as follows:
[
{
"package": {
"hostname": "ca.metrc.com",
"label": "1A4400001234000000005678",
"item": {
"name": "Bubba Kush",
...
},
"isInTransit": false,
"isOnHold": false,
"isOnRecall": false,
...
}
},
{
"package": {
"hostname": "ca.metrc.com",
"label": "1A4400001234000000005679",
"item": {
"name": "1g Wedding Cake Preroll - 5pk",
...
},
"isInTransit": false,
"isOnHold": false,
"isOnRecall": false,
...
}
}
]
How to add these values to a label is covered in Injecting Values
Default Data¶
Default data is used in two situations:
- You want to inject a value into all labels, but don't want to repeat it in every data list entry. (Example: facility address, shared between labels)
- Items in your data list are missing values, and you want to fall back to a specific value
Default data is passed in the commonContentData field. Its keys are merged into the top level of every label's data, so they are read exactly as if you had repeated them in each data list entry, a commonContentData of {"facilityAddress": "123 Main St"} is used as {{ facilityAddress }}.
A key set on an individual data list entry takes precedence over the shared value of the same name, which is what makes the fallback case work.
Default data is usually not needed.
Built-In Data¶
Built-in data is provided by T3 out of the box. It includes text and images that are commonly used by cannabis businesses, and is accessed under the t3.* namespace.
Examples:
- State-specific warning images (e.g.
{{ t3.images['ca_cannabis_warning.png'] }}) - State-specific warning disclosures (e.g.
{{ t3.text.ca_cannabis_warning_disclosure }}) - Generic cannabis images
- Poison Control Center contact information (
{{ t3.text.poison_control_center_phone }})
You don't have to manage any of these yourself; they're available in every label generation request.
State Disclosures¶
Most states require specific warning language on cannabis packaging. T3 ships that text under t3.text, keyed by the state's two-letter abbreviation:
So a California label reads:
Warning text is available for AK, AZ, CA, CO, MA, MD, ME, MI, MN, MO, MT, NJ, NV, OH, OK, OR and RI.
Some states separate particular warnings into their own statement, which are keyed by topic so you can place them independently:
| Key | States |
|---|---|
<state>_cannabis_warning_disclosure | all of the above |
<state>_pregnancy_warning_disclosure | AZ, CO, MI |
<state>_delayed_onset_disclosure | CO, OR |
The state abbreviation matches the Metrc hostname your packages come from, so a package loaded from co.metrc.com pairs with co_cannabis_warning_disclosure. A layout printed in one state is therefore a one-key edit away from another.
GET /v2/labels/globals returns every built-in value currently available, which is the authoritative list.
State Images¶
Several states require a warning mark or a printed warning panel alongside the text. T3 ships those images under t3.images, keyed the same way the disclosures are, with the file extension in place of _disclosure:
So the Michigan warning panel is:
Place one with an IMAGE element:
{
"description": "California marijuana product warning image",
"elementType": "IMAGE",
"valueTemplate": "{{ t3.images['ca_cannabis_warning.png'] }}",
"xStartFraction": 0.67
}
Images are available for AK, CA, CO, MA, MD, ME, MI, MN, MO, MT, NJ, NV, NY, OH, OK, OR and RI.
Key names follow the same shape as the disclosures, with a topic and an optional variant:
| Part | Values |
|---|---|
| topic | universal_symbol, medical_label, cannabis_warning, not_safe_for_kids |
| variant | light, dark, when a state publishes both |
So Minnesota, which mandates three separate marks, offers:
t3.images['mn_universal_symbol_light.png']
t3.images['mn_medical_label_light.png']
t3.images['mn_cannabis_warning_light.png']
Each also has a _dark rendering for dark label stock.
GET /v2/labels/globals returns every key currently available, which is the authoritative list.
These are sized for label printing, not for artwork reproduction. Scale one up much beyond the area of a label and it will soften.
images and t3.images are different namespaces
Your own uploads are read as {{ images['logo.png'] }}. T3's built-ins are read as {{ t3.images['name.png'] }}. An upload never appears under t3.
Images¶
Images can be inserted into labels and rendered using an IMAGE layout element.
The following is an example of how image data might be passed:
This image can now be used as follows in a layout element:
Fragments¶
fragments works the same way as images, but for whole layouts instead of image data: a map of named layouts, sent alongside your request and referenced by name. Use it to share a block (a warning panel, a barcode with its tag beneath it) across many labels instead of copying it into each one.
A fragment is an ordinary label layout, so a layout returned by GET /v2/labels/label-content-layouts can be passed here unchanged:
{
"state_warning": {
"labelContentLayoutElements": [
{ "elementType": "TEXT", "valueTemplate": "{{ t3.text.poison_control_center_phone }}" }
]
}
}
Draw it with a FRAGMENT layout element, which sets where on the label it goes:
{
"elementType": "FRAGMENT",
"fragmentKey": "state_warning",
"yStartFraction": 0,
"yEndFraction": 0.35
}
The fragment's own elements are positioned relative to that rectangle. Fragments require renderingVersion 2.
See Reusing Layouts With Fragments for nesting, aspect ratio, and the limits that apply.
Settings¶
Label settings configure how your labels will appear. Settings used most often:
- How many copies of each label should be printed
- Reversing the print order
- Rotating your layout sideways by 90 degrees
- Enabling/disabling watermark, for trial/error that doesn't count against your monthly quota
- Enabling/disabling debug, for drawing boundaries around each layout element
- Enabling
strictTemplates, so expressions that would render blank fail loudly instead
strictTemplates is worth calling out because it changes how mistakes surface. Off (the default), an expression T3 cannot resolve renders as an empty string, forgiving for optional fields, but a typo produces a blank label with no warning. On, the same expression returns a 400 naming the element and line:
Turn it on while building a layout to catch typos; leave it off in production so optional fields stay forgiving. See Missing Values for the default filter and other ways to say that blank output is intentional.
Injecting Values¶
Most layout elements support a value template, a string that tells T3 what to render inside the element. Value templates are processed with Jinja, so you can interpolate values from your data, use conditionals, and apply filters.
Interpolating a value:
Values can come from these namespaces:
- Per-label data: each entry in your
labelContentDataList. If the entry is{"package": {"label": "1A44..."}},{{ package.label }}renders1A44.... - Shared data: values from Default Data, merged into the top level of every label. A
commonContentDatakeyfacilityAddressis read as{{ facilityAddress }}. images.*, base64-encoded images you supplied, accessed by filename. For example,{{ images['logo.png'] }}.t3.*, Built-In Data provided by T3.data.*, an alias for the whole per-label entry, so{{ data.package.label }}matches{{ package.label }}.
Conditionals:
Filters:
For a deeper walkthrough, see the tutorial's section on injecting values.
Arranging and Styling Text¶
TEXT and TABLE elements support a range of formatting options:
- Font:
paragraphFontName(e.g.Helvetica,Times-Bold,Courier),paragraphFontSize,paragraphSpacing. - Alignment:
horizontalParagraphAlignment(LEFT,CENTER,RIGHT),verticalParagraphAlignment(TOP,CENTER,BOTTOM). - Overflow handling:
paragraphTextResizeStrategy:ALLOW_OVERFLOW(default), text overflows the element.SHRINK_TEXT: font size is reduced until the text fits.TRUNCATE_TEXT: letters are removed from the end until the text fits; an ellipsis is appended.
Tables are defined with HTML and share the same font settings as TEXT elements. A basic example:
<table>
<tr><td><b>THC</b></td><td>{{ package.metadata.indexedLabResults.totalTHC.value }}</td></tr>
<tr><td><b>CBD</b></td><td>{{ package.metadata.indexedLabResults.totalCBD.value }}</td></tr>
</table>
For the full list of options, see Label Layouts → Textual Elements.
Next Steps¶
- Build a label from scratch with the Tutorial.
- Full reference for the two halves of a label: Label Templates and Label Layouts.
- Print your PDFs reliably across thermal, inkjet, and laser printers.
- Walk through the CSV-driven Retail ID example.
- Call the T3 API Label endpoints directly to generate labels from your own tooling.
- Start your free 30-day T3+ trial