Skip to content

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:

[
  {
    "firstName": "Mike",
    "lastName": "Smith"
  },
  {
    "firstName": "Jane",
    "lastName": "Jones"
  }
]

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:

t3.text.<state>_cannabis_warning_disclosure

So a California label reads:

{{ t3.text.ca_cannabis_warning_disclosure }}

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:

t3.images['<state>_<topic>.png']

So the Michigan warning panel is:

{{ t3.images['mi_cannabis_warning.png'] }}

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:

{
  "logo.png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAO..."
}

This image can now be used as follows in a layout element:

{
  "elementType": "IMAGE",
  "valueTemplate": "{{ images['logo.png'] }}"
}

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:

{
  "renderingOptions": {
    "renderingVersion": 2,
    "strictTemplates": true
  }
}

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:

{{ package.label }}

Values can come from these namespaces:

  • Per-label data: each entry in your labelContentDataList. If the entry is {"package": {"label": "1A44..."}}, {{ package.label }} renders 1A44....
  • Shared data: values from Default Data, merged into the top level of every label. A commonContentData key facilityAddress is 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:

{% if package.isOnHold %}ON HOLD{% endif %}

Filters:

{{ package.item.name | upper }}

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