Skip to main content

Developer Apps

Let other users connect their account to your app without pasting API keys.

Written by Ryan Kulp

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.

Register an app form

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.

A registered app showing its client ID, client secret and redirect URI

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:

The TRMNL consent screen for an app

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.

Developer apps and Connected agents in the Developer section

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.

Did this answer your question?