This is a translation. The German version is authoritative. Where this text differs from it, the German text applies. You can switch language at the bottom of the page.
Contents
Overview
v2 is the API of tellmelo for your own programs. Every request carries a key, every answer is JSON, and every list pages the same way. What happens behind a key runs through the same rules as a click in the application: rate limits, suspensions, withdrawn rights and the settings of this instance apply here just the same.
Every path begins with /api/v2 at this instance’s own address, the one used in the examples on this page.
Requests and answers are JSON in UTF-8. Field names, codes and values are English and stay English.
A key acts as your account and never beyond it: what you cannot see in the application, no key can read either.
https://tellmelo.com/api/v2
Quick start
Create a key under “Settings → App & data → API” and copy it. It is shown only once.
Ask the root with it. The answer says which version is running, what the key may do and how many requests it has left this minute.
From there on every path works the same way: the key in the header, JSON back, and for lists items and next.
Where to go next: /me for your own profile, /posts for what is public, /feed for what the application shows you.
Your key
There is exactly one key per account, bound to your user id. It begins with tm_key_, so that a scanner recognises it where it does not belong. It is shown exactly once, at the moment it is created; afterwards only its fingerprint is held here, and nobody can read it back to you, neither we nor you. Whoever mislays it creates a new one; the old one stops being valid at that moment.
You create it under “Settings → App & data → API”. It travels in the header of the request, as Authorization: Bearer tm_key_… or as X-Tellmelo-Key. Not in the address bar, because what stands there ends up in access logs, in the browser history and in the referer of every link.
Not in the address bar. What stands there ends up in the access log of every server in between, in the browser history and in the referer of every link. A key does not belong there.
The same key also loads images. Every url in an answer points to /api/media on this instance; send the key in the header there too, and the picture comes back, as far as your account may see it.
A key carries two rights, and every path in the tables below says which of them it needs. Two and not five: a right you cannot explain to yourself in one sentence is a right you tick without reading. And the honest line runs between looking and changing.
read: looking. The public posts, profiles, communities and tags, and your own account as you see it: feed, notifications, conversations, messages.
write: changing. Everything that happens under your name: posting, replying, voting, sending messages, following, joining, blocking, marking notifications as read.
What no key can do
No key reaches your account data: no password, no email address, no account kind, no place, no role, no deletion. Nor sessions, nor push devices, nor the keys themselves: one that could issue keys could no longer be switched off. Nor administration, nor community leadership, because dissolving a community or handing it on is a decision about other people’s posts. What the password guards, no key may do: a secret lying in a script on somebody else’s machine must not be able to do what the login can. Taking over an account still costs the login.
Requests and answers
JSON everywhere. Every answer is application/json in UTF-8, errors included. Only images come as images.
What you send. A POST carries its fields as a JSON object in the body. Query parameters fill in what the body does not name; where both name the same field, the body wins.
Times are whole numbers of milliseconds since 1 January 1970, UTC, the way the database holds them. There is no time zone to get wrong.
Ids are strings. Do not take them apart and do not rely on their form; compare them only as a whole.
Every field is always there. What does not exist is null, never missing; a count is always a number, a list always a list, empty if need be.
Texts are plain text, exactly as written. #tags, @names and links stay as they are, and so do the instance’s own emojis, as :name:; their pictures are listed under /emojis.
Images are addresses relative to this instance, /api/media?id=…. Load them with the same key in the header; without one the answer is 401.
Pages
Every list takes limit and cursor and answers with items and next. You continue by sending the next of the last answer back as cursor; when next is empty, the end is reached. A full page can still be the last one, and whoever stops only at an empty page asks once too often. The cursor is opaque: a place in a list, not a point in time. Do not take it apart and do not build one yourself; what is inside it may change without any path changing.
A page holds 20 entries without limit, 50 at most. A few lists are not paged at all: they say so at their path, and their next is always null.
Only what is new
Every answer carries an ETag. A program that asks again and again should send the last one back as If-None-Match: if nothing has changed since, the answer is 304 and carries no body. That is the difference between a list that crosses the wire every minute and one that crosses it when there is something in it, for your machine as much as for this one.
Two exceptions, both on purpose: /feed carries no ETag, because its order moves with time and every answer is different; and a write never answers 304. Two identical posts are two posts.
Limits
A key may make 120 requests per minute, 600 with the “Organization” plan, unless the administration has set a different number for it. The limit counts per key, not per account and not per address. Above it the answer is 429 and nothing has happened: the request was refused, not carried out. Wait a minute and send it again; a program that runs into it regularly should slow down rather than ask again at once.
Besides the key’s own limit, writes count against the same limits as in the application: how many posts, replies or relations in a few minutes. Those also answer 429 with the code rate_limited.
Headers
The names are the ones every client library already knows. None is required except the key.
What you send
Header
Meaning
Authorization
Carries the key: Bearer tm_key_…. The usual way.
X-Tellmelo-Key
The key, as an alternative to Authorization, for tools that use that header for something else.
Content-Type
application/json, for a request with a body.
If-None-Match
The ETag of the last answer. If nothing has changed since, the answer is 304 without a body.
What comes back
Header
Meaning
ETag
The fingerprint of this answer, marked as weak (W/). Send it back as If-None-Match.
X-RateLimit-Limit
How many requests this key may make per minute.
X-RateLimit-Remaining
How many of them are left in the current minute.
X-RateLimit-Reset
When the next minute begins, in seconds since 1970, UTC.
X-Tellmelo-Scope
What this key may do: read or read write.
Retry-After
With a 429: how many seconds to wait before asking again.
WWW-Authenticate
With a 401 for a missing key: Bearer, the way a key is expected.
Cache-Control
no-store: no proxy in between may keep an answer, because it depends on the key that asked. Your own program may still check it with the ETag.
When something does not work
Errors come as JSON with two fields: a fixed English code in error that your program can compare against, and a sentence in message for the person in front of it, plus the status code that belongs to it. The code stays; the sentence may change, and it may change in any language: a program that compares the sentence breaks on a day nobody announced.
{ "error": "rate_limited",
"message": "Too many requests. Try again in a minute.",
"limit": 120 }
Some errors bring one more field: docs with a 401 for a missing key, limit with a 429.
Status
Code
Meaning
400
bad_request
The request cannot be read, a required field is missing, or a value is not one of those allowed. The message says which.
401
key_missing
No key in the header.
401
key_invalid
The key is not valid: mistyped, replaced by a new one, or its account is suspended. Which of these is deliberately not said.
401
unauthorized
Refused as not signed in, for a reason other than the key. Rare, and a reason to look at the account behind the key.
403
scope_missing
The key lacks the right this path needs: usually write on a key that may only read.
403
account_data_locked
This touches your account data, which no key can reach: password, email address, deletion and the like.
403
forbidden
Your account may not do this: the same refusal as in the application, for example a withdrawn right or a block.
404
unknown_path
There is no such path, or not with this method.
404
not_found
The path exists, but what it names does not, or you may not see it. The two are not told apart.
409
conflict
It collides with what is already there, for example a name that is taken.
413
too_large
Too large: a text or a request beyond what this instance accepts.
422
unprocessable
Readable, but not possible in this form.
429
rate_limited
Too many requests. Nothing was done; wait and send it again.
500
internal_error
Something went wrong on our side, not on yours. It is logged in full on the server.
Versions
The version is in the path. As long as it says v2, these paths and their fields stay as they are; what is added comes alongside.
Additions raise the second number: 2.1 added /emojis and marking a conversation as read. 2.4 removed the contentWarning field again, because content warnings no longer exist. 2.5 removed the alt field on images, because image descriptions no longer exist. 2.6 adds the notification kind team for new work for the team. Every path says since which version it exists, and the root says which version is running.
Endpoints
All paths at a glance, then each one in detail: what it takes, an example request and what comes back.
A post needs text or a poll. Its length, the number of poll answers and how many posts in how much time are set by this instance; beyond them the answer is 400 or 429, with a message that says which.
Images cannot be attached through the API yet, only in the application.
Parameters
Name
Type
Meaning
textin the body
stringoptional
The text itself: of the post, the reply or the message.
communityin the body
stringoptional
The id of the community the post goes into. Without it, outside every community.
quotesin the body
stringoptional
The id of the post this one quotes.
pollin the body
string[]optional
The answer options of a poll, as a list of texts. How many are allowed is set by the instance.
Example request
curl -X POST \
-H "Authorization: Bearer tm_key_…" \
-H "Content-Type: application/json" \
-d '{"text":"The bees are back in the garden 🐝 #garden"}' \
"https://tellmelo.com/api/v2/posts"
The conversation under a post: read as a list that parentId turns into a tree, written by replying to the post or to a reply.
GET/api/v2/posts/{id}/replies
The replies to a post, as you see them: what a blocked account wrote stays out.
Right read
pageable
with ETag
since v2.0
The list is flat and in the order of writing; parentId makes it a tree. A page holds whole threads, so a reply never arrives without the one it answers. What an account you blocked wrote, or one that blocked you, is left out. Two keys can see different lists.
Parameters
Name
Type
Meaning
idin the path
stringrequired
The id of a post.
limitin the query
integeroptional
How many entries a page holds: 20 by default, 50 at most.
cursorin the query
stringoptional
Where to continue: the next of the previous answer. Without it, from the top.
Under a post in slow mode each person can reply once every 10 minutes; another reply gets 429 with the wait in the message. Slow mode does not apply to the author or to the accounts the author follows.
Parameters
Name
Type
Meaning
idin the path
stringrequired
The id of a post.
textin the body
stringrequired
The text itself: of the post, the reply or the message.
parentIdin the body
stringoptional
The id of the reply this one answers. Without it, straight to the post.
Example request
curl -X POST \
-H "Authorization: Bearer tm_key_…" \
-H "Content-Type: application/json" \
-d '{"text":"Same here, the lavender is full of them."}' \
"https://tellmelo.com/api/v2/posts/5f0c2a9e-8d41-4b7a-9c3e-2e61d0f4a7b1/replies"
Searches only what is public: posts outside communities, profiles and tags. limit counts per kind: 20 can bring up to 20 posts, 20 profiles and 20 tags.
Parameters
Name
Type
Meaning
qin the query
stringrequired
The words being looked for.
typein the query
stringoptional
Which kind of result. Without it, every kind.allpostsprofilestags
limitin the query
integeroptional
How many entries a page holds: 20 by default, 50 at most.
The instance’s own emojis, with the address of their pictures.
Right read
with ETag
since v2.1
In texts an emoji of this instance stands as :name:. Replace it with the picture from this list; a name that is not in it stays text: it was deleted or never existed.
Your own feed, the way the application puts it together.
Right read
pageable
since v2.0
No ETag here: the order moves with time, so every answer is different. A list once started still stays the same to its end: next carries the moment it began.
Parameters
Name
Type
Meaning
tabin the query
stringoptional
Which feed: for-you, following, latest or bookmarks. latest by default.for-youfollowinglatestbookmarks
tagin the query
stringoptional
Only posts carrying this tag. Without it, no filter.
limitin the query
integeroptional
How many entries a page holds: 20 by default, 50 at most.
cursorin the query
stringoptional
Where to continue: the next of the previous answer. Without it, from the top.
What happened around your account, readable page by page, so a program can catch up on what it missed.
GET/api/v2/notifications
Your own notifications, newest first, pageable, so a program can catch up on what it missed.
Right read
pageable
with ETag
since v2.0
Reading marks nothing as read; POST /notifications/read does. Several events of the same kind about the same post are gathered into one row, and more says how many.
Parameters
Name
Type
Meaning
limitin the query
integeroptional
How many entries a page holds: 20 by default, 50 at most.
cursorin the query
stringoptional
Where to continue: the next of the previous answer. Without it, from the top.
The same rules as in the application apply: whoever blocked you cannot be written to, and the other person’s settings count. The answer then says why.
Images cannot be attached through the API yet, only in the application.
Parameters
Name
Type
Meaning
within the path
stringrequired
The id of the account you are talking with. A conversation has no id of its own: it is the other account, the same value the inbox calls with.
textin the body
stringrequired
The text itself: of the post, the reply or the message.
replyToin the body
stringoptional
The id of an earlier message in this conversation that this one answers. Since 2.2.
Example request
curl -X POST \
-H "Authorization: Bearer tm_key_…" \
-H "Content-Type: application/json" \
-d '{"text":"See you on Saturday at the market!"}' \
"https://tellmelo.com/api/v2/conversations/a3e1f7c2-54b9-4d0e-8f16-7b2c9d4e1a05/messages"
Like, save, repost, follow, join and block: one path for all of them, set and taken back with active.
POST/api/v2/relations
Like, save, repost, follow, join, block, mute, depending on kind.
Right write
since v2.0
target is a post for like, save and repost, a community for join, an account for follow, block and mute, always by its id. active: false takes the relation back. Nobody can follow across a block, in either direction. Only the one who mutes can see a mute.
Parameters
Name
Type
Meaning
kindin the body
stringrequired
Which relation: like, save, repost, follow, join, block or mute (since 2.2).likesaverepostfollowjoinblockmute
targetin the body
stringrequired
What the relation points at, by its id: a post, a profile or a community, depending on kind.
activein the body
booleanoptional
Whether the relation should stand: true sets it, false takes it back.
A notification. One row can gather several events of the same kind.
Field
Type
Meaning
id
string
The id of the notification.
kind
string
What happened.
reply — Someone replied to you.
like — Someone likes your post.
repost — Someone reposted your post.
follow — Someone follows you.
mention — Someone mentioned you.
group_mention — Someone mentioned a group you lead; text is its name.
message — Someone wrote you a message.
scheduled — A scheduled post of yours has been published.
reminder — A reminder about a saved post is due.
report — What became of a report you sent in.
moderation — A decision about your account: a warning, a restriction, an appeal.
team — New work for the team, only for owners, admins and moderators: reports, appeals, submitted links, inquiries, verification requests and cancellations.
text
stringor null
Only for reports and moderation: the text that comes with it. Otherwise null: the sentence for a notification is built by your program.
The id of the message this one answers (null if it answers none). Since 2.2.
createdAt
integer
When it was sent.
Example
{
"id": "7c6b5a49-3827-4165-9f0e-d1c2b3a49586",
"text": "See you on Saturday at the market!",
"from": "a3e1f7c2-54b9-4d0e-8f16-7b2c9d4e1a05",
"to": "d4c3b2a1-6e5f-4a7b-9c8d-1e2f3a4b5c6d",
"read": true,
"media": [],
"replyTo": null,
"createdAt": 1758624060000
}
Created
The answer of a write that created something.
Field
Type
Meaning
id
string
The id of what was created.
scheduledFor
integeror null
When it appears, if publishing was scheduled (otherwise null).
deleteAt
integeror null
When it deletes itself, if that was set (otherwise null).
The answer of a write that has nothing to hand back.
Field
Type
Meaning
ok
boolean
Always true.
Example
{
"ok": true
}
The machine-readable description
The same table this page is built from is served at /api/v2/openapi.json: paths, parameters, rights and the shapes that come back. A client generator can read it, and it cannot drift away from the API, because the page, the router and the description all come from one list.
https://tellmelo.com/api/v2/openapi.json
What became of v1
v1 has been removed. The old paths under /api/v1/ answer 410 and say in the body where to go instead. No redirect, because v2 answers in a different shape, and a program that followed one would get a 200 it cannot read. There were no public accounts yet at that point, and so nobody with a program on it; removing it later would have meant never removing it.
What we expect
The same guidelines as everywhere else: no harassment, no spam, no one else’s content without the right to it. A program excuses nothing: you answer for what your key writes.