Building something other TRMNL owners will use? A website, a mobile app, a home automation integration? Instead of asking each person to paste an API key into your app, let them click "Connect with TRMNL."
They sign in to TRMNL, choose what your app can do, and you get a token for their account. They can revoke it any time without touching their other keys.
If you're only automating your own account, you don't need any of this. An account API key is quicker.
Prerequisites
A TRMNL account with at least 1 device
A redirect URI, the address in your app where people land after connecting
Step 1 - Register your app
Navigate to Account -> Developer -> Manage developer apps, or go straight to trmnl.com/developer_apps. Click Register an app.
App name - People see this when they connect, so use the name they know you by.
Redirect URIs - One per line. Must be https, except localhost which can use http on any port.
Cannot keep a secret - Tick this for mobile, desktop and single-page apps, where your code runs on the person's own device. You won't get a client secret.
Click Register an app. On the next page you'll see your Client ID and Client Secret. Keep the secret on your server, never in code that ships to people's devices.
Step 2 - Send people to TRMNL
When someone clicks "Connect with TRMNL" in your app, send them to https://trmnl.com/oidc/authorize with these parameters:
response_type - code
client_id - your Client ID
redirect_uri - one you registered, exactly as you typed it
scope - the scopes below, separated by spaces
state - a random value you'll check when they come back
code_challenge and code_challenge_method - S256. This is PKCE, required if your app has no secret. We recommend it either way.
To make a PKCE pair from a terminal:
verifier=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64)
challenge=$(printf %s "$verifier" | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=\n')
Put together, the link looks like this (split across lines so it is easier to read):
https://trmnl.com/oidc/authorize?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Ftrmnl%2Fcallback
&scope=read%20content
&state=abc123
&code_challenge=YOUR_CODE_CHALLENGE
&code_challenge_method=S256
Here are the scopes you can ask for:
read - devices and their logs, playlists, plugin settings and private plugin files
content - change markup, plugin settings, playlists and mashups, install recipes, push images
devices - change device settings, identify a device
delete - delete plugin settings and playlist items, clear a device playlist
profile - see and change their name, time zone and display settings
apps - install and manage apps, including fleet pushes and room bookings
openid and email - sign them in to your app
Ask only for what you'll use. Here's what people see:
They can untick anything (delete, profile and apps start unticked), and they can limit your app to specific devices and plugin settings. So build for getting less than you asked for.
Step 3 - Swap the code for a token
After they click Allow, TRMNL sends them back to your redirect URI with a code and your state. Check the state matches, then POST to https://trmnl.com/oidc/token with:
grant_type - authorization_code
code - the code you received
redirect_uri - the same one from Step 2
client_id and client_secret - skip the secret if your app doesn't have one
code_verifier - the PKCE value you made the challenge from
For example:
curl -X POST https://trmnl.com/oidc/token \
-d grant_type=authorization_code \
-d code=THE_CODE_YOU_RECEIVED \
-d redirect_uri=https://example.com/auth/trmnl/callback \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code_verifier=$verifier
And the response:
{
"access_token": "bllcKj...",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "UvsaA-...",
"scope": "read content",
"created_at": 1790663000
}
You'll get back an access token, a refresh token and a scope field, plus an id_token if you asked for openid. The scope field is what they actually allowed, which may be less than you asked for.
Step 4 - Call the API
Send the access token as a bearer token to any endpoint under https://trmnl.com/api. The full list lives at trmnl.com/api-docs.
curl https://trmnl.com/api/devices \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response (trimmed):
{
"data": [
{
"id": 65007,
"name": "Kitchen",
"friendly_id": "ATVDJW",
"mac_address": "••:••:••:••:00:04",
"firmware_version": "1.6.3",
"battery_voltage": 4.0,
"refresh_interval": 900
}
]
}Access tokens last 2 hours. To get a new one, POST to the same token URL with grant_type=refresh_token, your refresh_token, your client_id and your client_secret (if you have one). You'll get a new refresh token too, so save both.
curl -X POST https://trmnl.com/oidc/token \
-d grant_type=refresh_token \
-d refresh_token=YOUR_REFRESH_TOKEN \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET
Building an AI agent instead? MCP clients like Claude Code register themselves, no developer app needed. Details here.
Step 5 - You're Done!
People see every connected app under Account -> Developer -> Connected agents. From there they can click Edit access to change which devices and plugin settings it reaches, or Revoke to disconnect it.
To give an app more scopes, they revoke it and connect again. And if you delete your developer app, every connection it holds goes with it.
Troubleshooting
"The requested redirect URI is malformed or doesn't match the client redirect URI." Your redirect_uri has to match a registered one character for character, trailing slash included.
A 403 saying the connection "lacks" a capability. They didn't allow that scope, or you didn't ask for it. Reading their profile needs profile, for example. Send them through Step 2 again.
{
"error": "This connection lacks the profile capability. Ask the user to connect it again with that capability."
}
A 403 naming a device or plugin setting. They limited your app to others. They can widen it from Connected agents -> Edit access.
A 401. Usually the token is more than 2 hours old, so refresh it. If the refresh fails too, they revoked your app and need to connect again. A token with only openid and email also gets a 401, since the API needs at least 1 other scope.
The connect link lands on your dashboard instead. You're signed in to someone else's account as a team member. Switch back to your own first.
Stay focused.




