# Table of Contents
- [BYOD | TRMNL API](#byod-trmnl-api)
- [Screen Templating | TRMNL API](#screen-templating-trmnl-api)
- [Introduction | TRMNL API](#introduction-trmnl-api)
- [Screen Templating (Graphics) | TRMNL API](#screen-templating-graphics-trmnl-api)
- [How it Works | TRMNL API](#how-it-works-trmnl-api)
- [Create a screen | TRMNL API](#create-a-screen-trmnl-api)
- [BYOD/S | TRMNL API](#byod-s-trmnl-api)
- [Introduction | TRMNL API](#introduction-trmnl-api)
- [BYOS | TRMNL API](#byos-trmnl-api)
- [Plugin Creation | TRMNL API](#plugin-creation-trmnl-api)
- [Fetch Screen Content | TRMNL API](#fetch-screen-content-trmnl-api)
- [Overview | TRMNL API](#overview-trmnl-api)
- [Getting Started | TRMNL API](#getting-started-trmnl-api)
- [Going Live | TRMNL API](#going-live-trmnl-api)
- [Plugin Screen Generation Flow | TRMNL API](#plugin-screen-generation-flow-trmnl-api)
- [Plugin Management Flow | TRMNL API](#plugin-management-flow-trmnl-api)
- [Introduction | TRMNL API](#introduction-trmnl-api)
- [Plugin Installation Flow | TRMNL API](#plugin-installation-flow-trmnl-api)
- [Plugin Uninstallation Flow | TRMNL API](#plugin-uninstallation-flow-trmnl-api)
- [Introduction | TRMNL API](#introduction-trmnl-api)
- [Provisioning Devices | TRMNL API](#provisioning-devices-trmnl-api)
---
# BYOD | TRMNL API
Before diving into "how," it's worth mentioning that **the investment required to build your own device will be greater than our retail price**.
Making your own TRMNL from scratch is not an economically rational decision, but rather a labor of love. Our own team learned this the ~hard~ fun way while building v1 over 7 months, from Dec 2023 to July 2024.
Here's what you can expect to spend per component:
* Battery, $5 (unnecessary if you prefer plugged in)
* EPD screen, $65 (see the Waveshare 7.5" on Amazon)
* Microcontroller, $3-50 (depends if you build/solder yourself or leverage a PCB prototyper)
* Enclosure/case, $3-20 (design + 3D print yourself or use a print farm)
If you know what you're doing and just need some firmware or a server client, here ya go:
* Firmware: [https://github.com/usetrmnl/firmware](https://github.com/usetrmnl/firmware)
* Server (multiple options): [https://docs.usetrmnl.com/go/diy/byos](https://docs.usetrmnl.com/go/diy/byos)
When you're ready to build plugins:
1. Buy access to the TRMNL web app + API: [https://shop.usetrmnl.com/products/byod](https://shop.usetrmnl.com/products/byod)
2. Activate a virtual device: [https://usetrmnl.com/claim-a-device](https://usetrmnl.com/claim-a-device)
3. Plug your TRMNL API key into your DIY device firmware
4. Stay focused
Email team@usetrmnl.com if you have any questions.
###
[](#hardware-requirements)
Hardware requirements
(_Coming Soon_)
[PreviousIntroduction](/go/diy/introduction)
[NextBYOD/S](/go/diy/byod-s)
Last updated 20 days ago
---
# Screen Templating | TRMNL API
[PreviousHow it Works](/go/how-it-works)
[NextScreen Templating (Graphics)](/go/private-plugins/templates-advanced)
Last updated 2 months ago
[](#overview)
Overview
---------------------------
The TRMNL device is an **800x480 pixel, black and white, 1-bit grayscale display**. This means we had to abandon a lot of modern web styling techniques when developing the API. Learn more about this process [here](https://usetrmnl.com/blog/design-system)
.
For the latest documentation on building beautiful plugins with TRMNL, see our Framework docs:
[https://usetrmnl.com/framework](https://usetrmnl.com/framework)
###
[](#quickstart-trmnl-account)
Quickstart (TRMNL account)
The easiest way to start building with TRMNL is by [making a Private Plugin](https://usetrmnl.com/plugin_settings?keyname=private_plugin)
from inside your account. This includes an inline editor, merge variable interpolation, and a live previewer.
###
[](#quickstart-no-trmnl-account)
Quickstart (no TRMNL account)
Create an HTML file with our plugins CSS + JS embedded in the `
`.
The example below has simple markup for a "full" layout plugin. We also offer half vertical, half horizontal, and quadrant sized layouts.
Copy
If this is saved into a [Private Plugin](https://usetrmnl.com/plugin_settings?keyname=private_plugin)
> Markup field, the following screen will be rendered:
As you can see, this isn't pretty (yet).
Here's another line chart with TRMNL-friendly styling:
Get all the code + learn how to do this here: [https://usetrmnl.com/framework/chart](https://usetrmnl.com/framework/chart)
[](#more-charts-and-graphs)
More Charts and Graphs
-------------------------------------------------------
Our [Framework docs](https://usetrmnl.com/framework)
are the best place for the latest examples and tips to improve the look and feel of graphical embeds from 3rd party tools like Highcharts.
[PreviousScreen Templating](/go/private-plugins/templates)
[NextCreate a screen](/go/private-plugins/create-a-screen)
Last updated 4 months ago
Un-styled chart example
Styled line chart example


---
# How it Works | TRMNL API
[PreviousOverview](/go)
[NextScreen Templating](/go/private-plugins/templates)
Last updated 8 months ago
The TRMNL **web server** hosts a growing directory of [native plugins](https://usetrmnl.com/integrations)
and API endpoints + templating engine for **custom plugins**, managed directly by customers. Learn to build a custom plugin [here](https://help.usetrmnl.com/en/articles/9510536-custom-plugins)
.
The TRMNL **device** is a custom PCB featuring an ESP32-C3 microcontroller, 1800-2500 mAh battery, and 7.5" EPD screen housed in injection-molded ABS soft touch plastic. Customers may disassemble their device, mod their firmware, and retrieve their API keys (_or pay a small one-time fee to unlock the Developer add-on_) without impacting our [Terms of Service](https://usetrmnl.com/terms)
.
TRMNL **firmware** supports automatic OTA (over the air) updates to WiFi-connected devices and is [open source](https://github.com/usetrmnl/firmware)
. Here's how it works:
1. TRMNL device wakes up and sends request to web server for displayable content every _n_ period\*
2. TRMNL web server generates a single-use, 1-bit, bitmap image (800x480 pixels). Response JSON includes a link to this image and timing instructions for the next "refresh" request.
3. TRMNL device renders the content, then goes to sleep for the instructed amount of time.
\* "Displayable content" is the most recently created screen, in order of priority according to the [Playlists](https://usetrmnl.com/playlists)
interface. "N" is a value in minutes, configurable by customers from the [Devices > Edit](https://usetrmnl.com/devices/)
web interface.
[](#opinionated-device-less-than-greater-than-server-relationship)
Opinionated device <> server relationship
-----------------------------------------------------------------------------------------------------------------
Most IoT products support SSH-ing directly into peripheral devices. We've heard too many horror stories about how this can go wrong, and decided to invert the paradigm.
**Your TRMNL device pings our server, never the other way around**.
Each request made to our `/api/display` endpoint includes only the minimum details needed to support customers -- an API key, device mac address, firmware version, battery voltage, and wifi signal strength.
**We do not collect any footprint of your location or identity**, such as IP address or WiFi configuration. The SSID and password for your local network are stored only on your TRMNL device.
When the TRMNL web server responds to a device's request we include only a few fields. These include "update\_firmware" (true/false), a direct download link to the firmware's \[public\] binary package, and whether the device should be reset. Customers may reset their device to transfer ownership or safely destroy data from their web account.
**TRMNL does not store rendered content over time.**
Whenever the web server generates a new bitmap image, it replaces the previous image. This keeps our costs low, allowing us to provide perpetual service without subscription fees. This also protects users, because we only have access to the most recent screen rendered for each of your plugins.
Device, Server, Native Plugins, 3rd Party Developer, Firmware Components

---
# Create a screen | TRMNL API
[PreviousScreen Templating (Graphics)](/go/private-plugins/templates-advanced)
[NextIntroduction](/go/diy/introduction)
Last updated 5 months ago
###
[](#before-you-begin)
Before you begin
Creating screens requires a Private Plugin instance inside your TRMNL account. This is currently available via the web interface only. Simply navigate to [Plugins > Private Plugin > New](https://usetrmnl.com/plugin_settings/new?keyname=private_plugin)
.
**Only devices with the Developer add-on** may access their own Access Token. You may unlock this feature anytime from your [Devices > Edit](https://usetrmnl.com/devices/)
page for a one-time fee.e a screen (webhook strategy)
If your private plugin's "Strategy" is set to Webhook, you can provide data to TRMNL's server at any time.
Simply send a `POST` request to the instance's Webhook URL, accessible from the configuration form. The below example updates the Plugin with UUID "asdfqwerty1234" using dynamic values inside a "merge\_variables" node:
Copy
curl "https://usetrmnl.com/api/custom_plugins/asdfqwerty1234" \
-H "Content-Type: application/json" \
-d '{"merge_variables": {"text":"You can do it!", "author": "Rob Schneider"}}' \
-X POST
The Plugin's UUID value (`asdfqwerty1234` in the example above) will associate your payload with the Markup you already provided when you [created your private plugin](https://help.usetrmnl.com/en/articles/9510536-custom-plugins)
.
To retrieve your Plugin's Webook URL / UUID, visit your private plugin instance. Note that you must "save" (create) the private plugin before a UUID and Webhook URL will be generated.
[](#create-a-screen-polling-strategy)
Create a screen (polling strategy)
-----------------------------------------------------------------------------
If your private plugin's "Strategy" is set to Polling, the TRMNL server will periodically fetch for new data from an endpoint of your choice.
Simply provide a "Polling URL" inside your private webhook instance (web UI), and TRMNL will make a `GET` request to that URL. To test this quickly, we've prepared an endpoint that responds with `text` and `author` key/value pairs, along with a `collection` array for quick demonstration:
[https://usetrmnl.com/custom\_plugin\_example\_data.json](https://usetrmnl.com/custom_plugin_example_data.json)
If your desired polling URL contains a collection/array in the root node, TRMNL will nest it inside a key named "data" for accessibility by the Liquid templating engine. Here is an example of that style payload:
[https://usetrmnl.com/custom\_plugin\_example\_data.json?collection\_only=true](https://usetrmnl.com/custom_plugin_example_data.json?collection_only=true)
**Note**: With the Polling strategy, variables _do not_ need to be nested within a "merge\_variables" node. That is only a requirement for the Webhook strategy.
[](#troubleshooting)
Troubleshooting
-----------------------------------------
For more assistance, see our [Private Plugin Tutorial](https://help.usetrmnl.com/en/articles/9510536-custom-plugins)
or email team@usetrmnl.com.
Private Plugin Webhook URL w/ UUID

---
# BYOD/S | TRMNL API
In the BYOD/S model, the only TRMNL IP is our [open source firmware](https://github.com/usetrmnl/firmware)
. Technically we don't owe you any explanation to get up and running, but we'll do it anyway. ;)
###
[](#device-setup)
Device setup
See our [BYOD guide](/go/diy/byod)
for instructions to build a device that's compatible with our firmware.
###
[](#server-quickstart)
Server quickstart
The TRMNL web server generates bitmap images. When a device pings our web server, the next-in-queue image is shared as an absolute URL inside a JSON response like this:
Copy
{
"image_url"=>"https://trmnl.s3.us-east-2.amazonaws.com/path-to-img.bmp"
}
You can demo this process from the command line by installing [ImageMagick](https://en.wikipedia.org/wiki/ImageMagick)
, then invoking `convert`:
Copy
convert regular_img.png -depth 1 eink_compatible.bmp
With this in mind, building your own server simply necessitates creating an endpoint that responds with links to firmware-compatible Bitmap images.
Assuming you've set up a device, simply...
1. change the base URL to your own server or local network from the WiFi Captive Portal
2. mimic the `api/setup` and `api/display` endpoints to respond per the [firmware readme](https://github.com/usetrmnl/firmware)
3. profit
###
[](#other-infrastructure)
Other infrastructure
In the quickstarter above we glossed over a critical element: "next-in-queue" images.
At TRMNL we use a Playlists table to manage the ordering of plugin instances, so that users can drag/drop different screen content in any sequence they like.
Each item in a device's Playlist is an instance\* of a Plugin, something we call a PluginSetting.
This keeps our Plugins table immutable, for example our Google Calendar record contains just name, icon, etc. But details about an actual connection to Google Calendar are stored inside a PluginSetting record. Keep this in mind as you build your own server -- do you want to allow multiple connections to the same parent plugin?
TRMNL also supports PlaylistGroups. These are parent objects to playlists, and they simply act as buckets to which playlist items should be rendered on a device during any given time period.
This is another feature to consider building on your own implementation, but does not need to be considered at the device or firmware level.
[PreviousBYOD](/go/diy/byod)
[NextBYOS](/go/diy/byos)
Last updated 1 month ago
Drag/Drop Playlists UI
Playlist Groups in action


---
# Introduction | TRMNL API
Our plugin marketplace allows developers to create their own public plugins for other users to install. In the future, the plugin marketplace will enable developers to earn money per install, similar to the Shopify App Marketplace or Google Play Store.
TRMNL's plugin marketplace uses the OAuth2 flow. The plugin owner maintains user data and is responsible for data privacy and security. TRMNL will fetch markup content at regular intervals and generate an image for the connected user's device to display.
[PreviousBYOS](/go/diy/byos)
[NextPlugin Creation](/go/plugin-marketplace/plugin-creation)
Last updated 6 months ago
---
# BYOS | TRMNL API
[PreviousBYOD/S](/go/diy/byod-s)
[NextIntroduction](/go/plugin-marketplace/introduction)
Last updated 8 hours ago
First, purchase a TRMNL from our [home page](https://usetrmnl.com)
.
Next, follow along this 10 minute video to bootstrap your own server client with ~zero coding required.
**Update** following this video's release —
Modifying firmware is no longer required. Simply set a custom base URL from the WiFi Captive Portal while provisioning credentials during Step 2, [here](https://help.usetrmnl.com/en/articles/9416306-how-to-set-up-a-new-device)
.
**Why BYOS?**
TRMNL intends to ensure that **every device is un-brickable and can run with zero external dependencies**.
###
[](#implementations)
Implementations
We support multiple implementations developed by us and the community at large. The goal isn't for BYOS to match parity with our hosted solution but to provide enough of a pleasant solution for your own customized experience. There are trade offs either way but we've got you covered for whatever path you wish to travel.
**Legend**
Use this legend to understand the matrix of features below.
* 🟢 Supported.
* 🟡 Partially supported.
* 🔴 Not supported or not implemented.
* ⚫️ Archived with minimal maintenance support.
* ⚪️ Unknown.
**Matrix**
Below is a list of all implementations in various languages/frameworks you can use to self-host and manage your devices with:
Implementation
Dashboard
Auto-Provisioning
Devices
JSON Data API
Image Previews
Playlists
Plugins
Recipes
Docker
Maintained
Semantic Versioning
🟢
🟢
🟢
🟢
🟡
🔴
🔴
🔴
🟢
🟢
🟢
🟡
🔴
🟡
🟢
🔴
🔴
🔴
🔴
🟢
⚫️
🟢
🔴
🔴
🟢
🟢
🟢
🟢
🔴
🔴
🔴
🔴
⚪️
🔴
🔴
🔴
🟡
🔴
🔴
🔴
🔴
🔴
🔴
⚪️
🟢
🟢
🟢
🟢
🟢
🟢
🟢
🔴
🟢
🟢
🔴
🟢
🟢
🟢
🟢
🟢
🔴
🔴
🟢
🔴
🟢
⚪️
Here's a more detailed breakdown of each feature:
* **Dashboard**: Provides a high level overview of information.
* **Auto-Provisioning**: Means that a device can be automatically provisioned once added to your network. This includes the automatic provisioning of new and existing devices.
* **Devices**: Provides device management in terms of updating each device, viewing current image, viewing logs, and more.
* **JSON Data API**: Provides full API support using a JSON Data API for device management, image generation, logging, etc.
* **Image Previews**: Provides a UI for viewing generated images for devices, plugins, recipes, etc.
* **Playlists**: Supports hosted playlist feature.
* **Plugins**: Supports hosted plugins feature.
* **Recipes**: Supports hosted recipes feature.
* **Docker**: Supports Docker for both local development and production deployment.
* **Maintained**: Project is maintained and kept up-to-date on a weekly basis.
* **Semantic Versioning**: Supports [strict semantic versioning](https://alchemists.io/articles/strict_semantic_versioning)
.
###
[](#api)
API
At a minimum, the following API endpoints should be supported for all BYOS implementations:
####
[](#display)
Display
Copy
curl "http://byos.local/api/display" \
-H 'ID:
' \
-H 'Access-Token: ' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json'
####
[](#images)
Images
Copy
curl -X "POST" "http://byos.local/api/images" \
-H 'ID: ' \
-H 'Access-Token: ' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d $'{
"image": {
"content": "Demo
"
"file_name": "demo"
}
}'
####
[](#logs)
Logs
Copy
curl "http://byos.local/api/log" \
-H 'ID: ' \
-H 'Access-Token: ' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json'
####
[](#setup)
Setup
Copy
curl "https://byos.local/api/setup/" \
-H 'ID: ' \
-H 'Access-Token: ' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json'
💡 For a detailed breakdown of all API endpoints and what they can do, please refer to the [Terminus API Documentation](https://github.com/usetrmnl/byos_hanami?tab=readme-ov-file#apis)
.
(Ruby + Hanami)
(Ruby + Sinatra)
(Elixer + Phoenix)
(Python + Django)
(PHP + Laravel)
(Next.js)
[**TRMNL Terminus**](https://github.com/usetrmnl/byos_hanami)
[**TRMNL**](https://github.com/usetrmnl/byos_sinatra)
[**TRMNL**](https://github.com/usetrmnl/byos_phoenix)
[**Community**](https://github.com/usetrmnl/byos_django)
[**Community**](https://github.com/usetrmnl/byos_laravel)
[**Community**](https://github.com/usetrmnl/byos_next)
BYOS - Open Source TRMNL Project
---
# Plugin Creation | TRMNL API
[PreviousIntroduction](/go/plugin-marketplace/introduction)
[NextPlugin Installation Flow](/go/plugin-marketplace/plugin-installation-flow)
Last updated 6 months ago
You can create a new plugin by visiting the following URL:
Copy
https://usetrmnl.com/plugins/my/new
You'll have to provide the following information about your plugin.
**Name**: Branded title (if applicable, ex "Vandelay Industries") or brief tag that describes the plugin's functionality
**Description**: Additional text to help differentiate your plugin from others
**Icon**: PNG format preferred
**Installation URL**: Endpoint where TRMNL should trigger the installation flow
**Installation Succes Webhook URL**: Where you want to receive installation success events as a webhook
**Plugin Management URL**: Where TRMNL users can manage their plugins
**Plugin Markup URL:** Endpoint where TRMNL should ping your webserver for markup content
**Uninstallation Webhook URL**: Where you want to receive uninstallation events as a webhook
TRMNL public plugin client

---
# Fetch Screen Content | TRMNL API
First, purchase + set up a TRMNL device.
Alternatively you can pay a small subscription fee for server-only access. With this setup you'll provide your own hardware but have full access to the TRMNL web application for plugin management, content rendering, and all the other features enjoyed by TRMNL device owners. Email support@usetrmnl.com for more information.
Once you have an API Key -- accessible from your [Devices > Edit](https://usetrmnl.com/devices)
> Developer Perks -- make a request like below. Just replace "xxxxx" with your API key.
Copy
curl https://usetrmnl.com/api/display --header "access-token:xxxxxx"
This will respond with several fields, for example:
Copy
{
"status"=>0, # will be 202 if no user_id is attached to device
"image_url"=>"https://trmnl.s3.us-east-2.amazonaws.com/path-to-img.bmp",
"image_name"=>"2024-12-15T00:00:00",
"update_firmware"=>false,
"firmware_url"=>nil,
"refresh_rate"=>"1800",
"reset_firmware"=>false
}
The `image_url` is likely the most interesting to you, as this may be leveraged by your own hardware to render content however you see fit.
**Note**: TRMNL devices send a few additional values in the request headers by default, such as your WiFi connection strength (RSSI value), firmware version (ex: 1.3.7), and more.
These atrributes impact the response content by instructing the device to either update firmware, change its refresh rate, and so on. But excluding these values from your request is OK, just be aware that some response values may be nil.
[PreviousIntroduction](/go/private-api/introduction)
[NextIntroduction](/go/partners-api/introduction)
Last updated 5 months ago
---
# Overview | TRMNL API
Last updated 1 month ago
👋
[How it Works](/go/how-it-works)
[Screen Templating](/go/private-plugins/templates)
[DIY TRMNL (Advanced)](/go/diy/introduction)
[Plugin Marketplace](/go/plugin-marketplace/introduction)
[Private API](/go/private-api/introduction)
[Partners API](/go/partners-api/introduction)
---
# Getting Started | TRMNL API
The TRMNL team manually approves each Partner, which involves the following:
1. basic KYC to ensure our intentions align (_customer delight, not bulk discounts_)
2. program negotiation (net-30 vs on-demand payment terms, optional quotas, etc)
3. testing the Partner's custom plugin end-to-end
**KYC** includes determining a Partner point of contact, getting some sense of weekly / monthly device provision volume, and possibly a small deposit.
**Program negotiation** is simple and might look like this: "_Partner wants to award their customers a TRMNL device for 50% off. Partner can generate TRMNL discount codes for 50% off and will pay TRMNL monthly via invoice for the remaining 50%, less a 10% bulk discount._"
**Testing** entails the Partner providing TRMNL a demo user account on the Partner platform, with instructions to set up their custom plugin manually on an existing TRMNL account.
[PreviousIntroduction](/go/partners-api/introduction)
[NextProvisioning Devices](/go/partners-api/provisioning-devices)
Last updated 3 months ago
---
# Going Live | TRMNL API
After building and testing your plugin, copy/paste the following application into an email to team@usetrmnl.com.
Subject:
Copy
Public plugin submission - {{ plugin name }}
Body:
Copy
Hi team,
Please review my plugin for the public marketplace:
Plugin ID: {{ visit My Plugins > click Edit, copy integer ID from URL }}
Owner Email: {{ should == inbox from which you are sending this message }}
Why should this plugin be public vs private? How does it help other users?
{{ should be different than your Description field. if your plugin is against our ethos (breeds distraction, not focus), it may be better as an open source + Private plugin. }}
Video demonstration:
{{ link to video, no audio required, of the plugin being installed from scratch. }}
How can we test this plugin works?
{{ preferably a demo login email/password that we can own forever, ex "team@usetrmnl.com" }}
Will you how promote TRMNL when this plugin is published? If so, how/where?
{{ no wrong answers, but we prioritize plugins that help us grow }}
Thanks,
{{ your name }}
After receiving this information we'll test out your plugin, send revision requests (if applicable), and soft launch it to our Plugins marketplace by changing the status to "public."
We can then discuss featuring your work in an upcoming email newsletter or other social channels. At any time you are welcome to post it in our developer-only Discord's "#flex" channel, prior to public approval.
[PreviousPlugin Uninstallation Flow](/go/plugin-marketplace/plugin-uninstallation-flow)
[NextIntroduction](/go/private-api/introduction)
Last updated 6 months ago
---
# Plugin Screen Generation Flow | TRMNL API
[PreviousPlugin Management Flow](/go/plugin-marketplace/plugin-management-flow)
[NextPlugin Uninstallation Flow](/go/plugin-marketplace/plugin-uninstallation-flow)
Last updated 1 month ago
TRMNL generates a screen every X minutes, where X is the refresh frequency set by the user.
TRMNL generates screens by sending a POST request to the `plugin_markup_url` endpoint you specified during [Plugin Creation](/go/plugin-marketplace/plugin-creation)
. The request body will include the `user_uuid` (that particular user's plugin connection UUID). The request header contains an `authorization` key with the user's plugin connection `access_token`as the Bearer token. Here's an example of our server request:
Copy
curl -XPOST 'https://your-server.com/your-markup-url' \
-H 'Authorization: Bearer xxx' \
-d 'user_uuid=xx'
Your web server should respond with HTML inside root nodes named `markup`, `markup_quadrant`, and so on to satisfy each layout offered by TRMNL. This markup should include whatever values you want the user to see rendered on their screen.
**Pro tip**: use the [Private Plugin](https://usetrmnl.com/plugin_settings/new?keyname=private_plugin)
markup editor to develop the frontend of your plugin. This in-browser text editor supports live refresh and automatically applies the correct styling and JavaScript helpers to your markup.
TRMNL uses the markup in your server's response to generate an e-ink friendly image. If the user connecting your plugin created a "full screen" playlist item, TRMNL will leverage the HTML inside the `markup` node. If they connected your plugin as part of a left/right Mashup, TRMNL will look for HTML inside the `markup_half_vertical` node.
Here's an example of a valid server response:
Copy
{
"markup": 'Daily ScriptureHello
World ', "markup_half_horizontal": '
Your content
', "markup_half_vertical": '
Your content
', "markup_quadrant": '
Your content
'
}
**Note:** in order for your plugin to be published in the TRMNL public marketplace, you must provide HTML for all available markup layouts. [View them here](https://help.usetrmnl.com/en/articles/10168132-mashups)
.

---
# Plugin Management Flow | TRMNL API
[PreviousPlugin Installation Flow](/go/plugin-marketplace/plugin-installation-flow)
[NextPlugin Screen Generation Flow](/go/plugin-marketplace/plugin-screen-generation-flow)
Last updated 6 months ago
After a user installs your plugin, they may want to manage the plugin settings. TRMNL will redirect the user to the `plugin_management_url` with a unique user identifier (UUID) params, so that you can identify them on your web server.
Example request: `https://yourapp.com/manage?uuid=ae48d6ac-48f4-4aed-8464-bad68368e97c`
**Note**: The UUID is the unique user identifier in the TRMNL plugin architecture. This allows TRMNL users to have multiple instances of the same plugin, each with their own settings.

---
# Introduction | TRMNL API
As outlined in our [open source firmware](https://github.com/usetrmnl/firmware)
, TRMNL exposes a GET endpoint that responds with image and other content for your device to store or render.
Copy
GET /api/display
# request headers example
{
'ID' => 'XX:XX:XX:XX',
'Access-Token' => '2r--SahjsAKCFksVcped2Q'
}
# response body example
{
"image_url"=>"https://trmnl.s3.us-east-2.amazonaws.com/path-to-img.bmp",
"image_name"=>"2024-09-20T00:00:00",
"update_firmware"=>false,
}
The TRMNL server leverages your device's immutable Mac Address (`ID` ) header during initial setup, then depends on the `Access-Token`header for subsequent requests.
Thus, **if you know your device's API key, you can request content without a TRMNL device or TRMNL firmware**.
In these \[WIP\] Private API docs we'll outline a few ways to take advantage of this information for your own privacy, security, and experimentation purposes.
**Only devices with the Developer add-on** may access their own Access Token. You may unlock this feature anytime from your [Devices > Edit](https://usetrmnl.com/devices/)
page for a one-time fee.
[PreviousGoing Live](/go/plugin-marketplace/going-live)
[NextFetch Screen Content](/go/private-api/fetch-screen-content)
Last updated 5 months ago
---
# Plugin Installation Flow | TRMNL API
[PreviousPlugin Creation](/go/plugin-marketplace/plugin-creation)
[NextPlugin Management Flow](/go/plugin-marketplace/plugin-management-flow)
Last updated 1 month ago
1. **Installation Request**
When the user installs your plugin, TRMNL sends an installation request to `installation_url` with unique `token` and `installation_callback_url`.
1. **Fetch Access Token**
After receiving the request, Your server using the `client_id`, `client_secret` and `token` from step#1 request the `access_token` from TRMNL using the following endpoint:
Copy
body = {
code: 'code-from-step-1',
client_id: 'your-plugin-client-id',
client_secret: 'your-plugin-secret',
grant_type: 'authorization_code'
}
response = HTTParty.post("https://usetrmnl.com/oauth/token", body: body)
response['access_token']
1. **Access Token**
TRMNL responds with the `access_token`.
1. **Installation Callback**
Use the `installation_callback_url` from Step #1 and redirect the user back to TRMNL.
1. **Success Webhook**
After the user has successfully finished installing the plugin, TRMNL sends a success notification to `installation_success_webhook_url` endpoint. Data is sent in JSON format as follows.
HTTP Headers:
Copy
{ 'Authorization': 'Bearer
', 'Content-Type': 'application/json' }
Body:
Copy
{
"user": {
"name":"Ronak J",
"email":"ronak@usetrmnl.com",
"first_name":"Ronak",
"last_name":"J",
"locale":"en",
"time_zone":"Pacific Time (US & Canada)",
"time_zone_iana":"America/Los_Angeles",
"utc_offset":-28800,
"plugin_setting_id":1234,
"uuid": "674c9d99-cea1-4e52-9025-9efbe0e30901"
}
}
Time zone mappings are available here under "Constants:" [https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html)
The `plugin_setting_id`is useful for building a redirect URI in your own application, for example to send a user back to usetrmnl.com/plugin\_settings/:plugin\_setting\_id/edit.

---
# Plugin Uninstallation Flow | TRMNL API
[PreviousPlugin Screen Generation Flow](/go/plugin-marketplace/plugin-screen-generation-flow)
[NextGoing Live](/go/plugin-marketplace/going-live)
Last updated 4 months ago
When a user uninstalls your plugin, as a best practice TRMNL will send a notification via webhook. The request is sent to the `uninstallation_webhook_url` in JSON format with the following details:
HTTP Headers:
Copy
{ 'Authorization': 'Bearer ', 'Content-Type': 'application/json'
Body:
Copy
{
"user_uuid": "uuid-of-the-user"
}
Parse this webhook payload to perform a "teardown" or similar strategy on your web server.

---
# Introduction | TRMNL API
If you're a company with a custom plugin, TRMNL makes it easy to reward customers with free or discounted devices that arrive pre-connected to your platform.
Example scenario:
1. Acme SaaS has a subscriber dashboard for their email newsletter tool. Acme builds a custom plugin inside TRMNL to showcase this information.
2. Acme wants to gift a free TRMNL device to their top customers. Acme creates coupon codes with the TRMNL Partners API that their customer can use at checkout for X% off their order.
3. When Acme's customer device is shipped, it is associated to their account on Acme's platform. Upon unboxing + WiFi pairing, Acme's customer will see their Acme native dashboard on TRMNL device without any additional setup.
Continue reading to learn how this works with just a single API request, or email [partners@usetrmnl.com](mailto:partners@usetrmnl.com)
to get started.
[PreviousFetch Screen Content](/go/private-api/fetch-screen-content)
[NextGetting Started](/go/partners-api/getting-started)
Last updated 3 months ago
---
# Provisioning Devices | TRMNL API
To preload a custom plugin on a device for your customer, you can generate a coupon code and share the customer's relevant credentials in a single API request.
**Step 1 - Partner requests a coupon**
Copy
POST /api/partners
Headers:
Client-ID, Access-Token # provided by TRMNL team
Body:
{
partner: {
action: "provision_discount",
data: { "user-data": "goes here", "more-data": "also ok" },
meta: { "expires_at": "2025-03-20" } # will expire at 23:59 EST on this date
}
}
Based on your program terms, TRMNL will generate a coupon with your Partner name + unique suffix.
Copy
# response example
{ status: 200, data: { code: "acme-123456789" } }
If a quota is preferred, for example 50x maximum provisions per month, this endpoint will return a `nil`code value when the quota is breached.
**Step 2 - TRMNL generates a coupon**
Provide the `code`from Step 1 to your customer with instructions to purchase a device from usetrmnl.com. They can provide this code at checkout.
If your discount is for 100% off, they will not be charged. If your code is for 50% off, they will pay 50% at checkout. You will be billed via invoice later for claimed discount codes during the agreed period. Additional terms are possible, for example requiring customers to pay for shipping, or only subsidizing a device with our regular (vs large size) battery, etc.
**Step 3 - TRMNL pre-loads the Partner's plugin**
Prior to this workflow being implemented, TRMNL should have already tested your custom plugin. Assuming it requires some kind of API credential to be accessed by a TRMNL user's device, the Step 1 payload should include these details inside the `data`node.
Whatever key/values are provided in the `data` node of Step 1 will be saved to the user's pre-loaded plugin when their device is unboxed and set up. Thus these key/values should match exactly the merge variables required by the plugin setting instance.
Contact [partners@usetrmnl.com](mailto:partners@usetrmnl.com)
with questions or requests.
[PreviousGetting Started](/go/partners-api/getting-started)
Last updated 2 months ago
---